Yuki's Tech Blog

仕事で得た知見や勉強した技術を書きます。

【2022/12/3 ~ 2022/12/20】コードレビューで知ったことをざっくりまとめてみた

目次

概要

フロントエンドのタスクをレビューしていただいた際に、知ったことをまとめます。

Open API

OpenAPI 3.1未満の場合はnullable: trueを追加しないとnullが許容されない

OpenAPI 3.1未満の場合はnullable: trueを追加しないとnullが許容されないため、nullable: trueを追加します、stoplight上でnullの表示を消しても良いのですが、分かりやすさのため、APIドキュメントにもnullを書きます。

Rails

Like句の後ろに半角スペースを入れる

Like句の後ろには半角スペースを入れます。

  products = Product.where("sku LIKE ? ", "#{params[:sku]}%")

procよりラムダを使う

ラムダの方が記述が楽なので、procをどうしても使わないといけない場面でなければ、ラムダを使います。

分類を表すカラム名はkind や categoryのような単語を使う

categoryは使いやすいので、kindをまずは優先して使いましょう。

Rubyではスネークケースを使う

Rubyではスネークケースを使うので、変数定義する時に間違えないように気をつけましょう。

Rspec

存在しない値でリクエストを出したい場合、もしかしたら存在しそうな値を使わない

  let(:id) { Faker::Number.number(digits: 10).to_s }

存在しない値でリクエストを出したい場合、以上のコードだとDBに存在するような値が生成される可能性があります。以下のように絶対に存在しない値(桁数を99桁)にするなどして、テストの信頼性を高めるようにします。

  let(:id) { Faker::Number.number(digits: 99).to_s }

無駄にクエリ発行させない

以下のコードではskuを取得するために、pluckでクエリを一回発行しています。

  before do
    create_list(:product, 3)
  end

  let(:sku) { product.pluck(:sku).sample }

しかし、before doの部分でデータを事前に作っているので、そのデータを利用してskuを取得した方が良いです。

  before do
    @products = create_list(:product, 3)
  end

  let(:sku) { @products[0][:sku]  }

素数を検証したい場合、 have_attributeを利用する

配列の要素数を検証したい場合、have_attributeを利用します。

expect(response.body).to be_json_including(data: {
  products: have_attributes(:count, 0)
})

React

無駄に改行しすぎない

無駄に改行することでファイルの行数が増えてしまうので、1行で行けるなら、1行で書きましょう。

z-50などの使う側の都合で決まる情報をコンポーネント自身に指定しない

コンポーネントの責務は基本的には、自分自身に関することです。z-50などは、コンポーネントを使う側の都合であって、それをコンポーネント自身に指定すると、そのコンポーネントを使う側の都合で決まる情報をコンポーネントが持ってしまい、再利用性が下がります(なんで使う側の都合で決まる情報を子コンポーネントが持たないといけないのか)。そして親コンポーネントの情報を持ってしまっているため、コンポーネント同士が独立していないです。そもそもReactでは複数の機能単位で分割された独立性の高いコンポーネントを組み合わせてソフトウェアを開発していきます。その際、1つのコンポーネントがさまざまな責務を背負っていたり、他のコンポーネントに依存していると、1つの責務を変更するたびに他の責務に影響がないかを細かく確認しないといけなかったり、依存しているせいで再利用性が下がる可能性があります(同じような機能をもう一度作る可能性もあるので)。1回限りしか使わないコンポーネントが増えていく未来しか見えず、同じようなコードが量産される原因になるので、独立したコンポーネントを書くように気をつけましょう。こういう親の都合で決まる値はpropsで子コンポーネントに渡します。そうすることで、親に値の情報を持たせつつ、子コンポーネントに渡すことができます。

複数のプロパティが存在する場合、型定義はインラインで行わない

複数のプロパティが存在する場合、型定義をインラインで行うと、見通しが悪くなります。そのため、型エイリアスで定義します。

■before

  const kind: {
    value: "food" | "dailyNecessities";
    label: string;
    checked: boolean;
  } = {
    // 省略

■after

  type Kind = {
    value: "food" | "dailyNecessities";
    label: string;
    checked: boolean;
  };

  const kind: Kind = {
    // 省略

空白を作りたい場合、cssでマージンを設定するか、flexのgapを利用する

特殊文字の空白を使うと意図が分かりづらいので、cssで指定した方が良いです。

■before

<div>
  {`${product.sku}\u00A0\u00A0${product.name}`}
</div>

flexのgapを使うことで、フレックスアイテム間にスペースを作ることができます。以下の場合、x方向にスペースを作っています。

■after

<div className="flex gap-x-4">
  <div>{sku}</div>
  <div>{name}</div>
</div>

Open API Generatorで生成したenumは特殊なので使用しない

自分で定義したenumを使用しましょう。理由が明確にはわかっていないので、今度遭遇した時に調べてみます。

React Hook Formで同じようなデータは配列で管理する

useFieldArrayを使うことで、配列データの要素の追加や削除、管理がしやすくなります。

細かすぎる粒度でコンポーネントを切らない

上記のようなコンポーネントを切るべき明確な理由を考えずに、細かすぎる粒度でコンポーネントを切らない方が良いです。コンポーネントの粒度が細かすぎると、その分コンポーネントのファイルが増えるので、複数のファイルを行ったり来たりして開発効率が落ちる未来が見えます。デメリットしかなくメリットがないのに、コンポーネントを切るのはやめましょう。

下記の記事でその内容が書かれていて分かりやすいです。

極端なパターンを考えて、技術的な落とし所を探っていく - mizdra's blog

Flagmentだけをレスポンスするのではなくnull なりをreturnする

React コンポーネントで何も返したくない場合、Flagmentではなくnullを返します。Flagmentはそもそも、JSXで最上位に複数の要素を配置できなくて、仕方なく1つの要素を配置していたものを解決するためのものです。Flagmentを使うことで、仕方なく配置していた1つの要素を指定しなくて良くなり、かつ余分なDOMが出力されません。Flagmentもそうですが、本来の目的で使うようにしましょう。

コンポーネント内で定義する関数は全てuseCallbackをつける

useCallbackとは、関数自体をメモ化するフックです。親コンポーネントで関数を定義していて関数を子コンポーネントにpropsとして渡している場合、親コンポーネントが再レンダリングされるたびに関数が再生成されるので、子コンポーネントに違う関数をpropsで渡していると判定されてしまいます。その結果、子コンポーネントが再レンダリングします。関数自体をメモ化することで、関数の再生成を防ぎ、子コンポーネントの再レンダリングを防ぐことができます。

クリックしたデータをDOMから参照せずに、onClickに動的生成した関数を渡して取得する

以下のコードでは、DOMを参照してデータを取得しているので、コードが複雑になりがちです。そして分かりづらいです。

■before

  const onClick = (e: MouseEvent<HTMLLIElement>) => {
    const clieckedSku = e.currentTarget?.firstChild?.firstChild?.textContent;
    // 省略

カリー化を使うことで動的に関数を生成して、その動的関数をonClickに渡すことができます。onClickにデータを渡したFactory関数呼び出しを指定することで、動的生成した関数にデータを渡すことができます。そしてその動的生成された関数自体がreturnされて、onClickにセットされます。

■after

const onClickFactory = useCallback((product: Product) => () => {
  // 本来したい処理
}, []);

// 使う場所
Products.map((product) => <li onClick={onClickFactory(product)}>{product.name}</li>;

カリー化を使わなくてもかけますが、同じ関数を使う場面でも無駄に() =>を書かないといけないかつ、Propsにアロー関数を直接渡すのは推奨されていない(useCallbackを使った関数を事前に定義する)ので、カリー化を使った方が良いでしょう。

const onClick = useCallback((product: Product) => {
  // 本来したい処理
}, []);

// 使う場所
Products.map((product) => <li onClick={() => onClick(product)}>{product.name}</li>;

ちなみにFactoryという名前はFactoryパターンから来ています。関数を生成するという意味でFactoryを使っていると思いますが、Factoryパターンをまだあまりわかっていないので、必要になったら調べます。

import時にindexを指定しない(indexは省略できる)

indexという名前は特殊扱いされていて、components/Todo/indexと書かなくても、components/Todoと書けばOKです。

フォームを作る際はバックエンド側のデータの持ち方になるべく準拠する

バックエンド側でのデータの持ち方をフロントエンド用に整形して扱うのはめんどくさいです。その場合、バックエンド側でのデータの持ち方が不適切な可能性があります。

まとめてエクスポートする場合、index.tsを使う

index.tsに以下のような記述をすることで、コンポーネントをまとめてエクスポートすることができます。

export * from "./Todo";
export * from "./TodoList";

相対パスで短く指定できるなら、そうする

相対パスで短く指定できる場合、そうした方が良いです。パスも短くなり可読性が上がります。

import { DefaultValues } from "./";

zodのバリデーション対象が配列の場合、z.arrayを先に書く

zodでは配列を検証する方法が2つ存在します。

const stringArray = z.array(z.string()); // => string[]

// equivalent
const stringArray = z.string().array(); // => string[]

配列であることを検証する場合、先に書いてある方が、シンプルで分かりやすいです。

{
  productLogs: z.array(z.object({
    ...
  })),
}

tailwindでdisabledを指定する場合、大元の汎用コンポーネントに指定する

スタイルを統一させたいためです。ちなみにtailwindでは以下のようにしてdisabled時のスタイルを当てることができます。

disabled:opacity-60

mutate関数を自前で用意するのではなく、useMutationから分割代入で取得する

react queryが自前で用意しないでいいようにしてくれているので、自前で作る必要はないです。

■before

  const create = useCallback(({
    data,
    onSuccess,
    onError,
  }: {
    data: Data,
    onSuccess: ({ data }: { data: PostProducts201ResponseAllOfData }) => void
    onError: () => void
  }) => {
    mutate.mutate(data, { onSuccess, onError });
    // 省略

■after

const { mutate: create } = useMutation((data: Data) => {
  return api.postProducts(data);
});

いろんなデータ操作系のhooksを同時に利用したい場合はほとんどないので、そういう時にだけ呼び出し側でmutate関数に別名をつければ良い

データ操作系のhooksを定義するとき、mutate関数にcreateProductcreateProductLog ~と名前をつけていました。そのような名前をつけるデメリットは、hooksを使うときにその名前で使わないといけないところです。名前が長いので、書くのが面倒です。1つのコンポーネントでデータ操作系のhooksを同時に呼び出すことはほとんどないので、基本mutatet関数の名前はcreateにしつつ、もし複数のデータ操作系のhooksが必要になった場合、呼び出し側で別名をつければOKです。

typeを使った型はコンポーネントの外側で定義する

コンポーネントの中で型定義すると、エクスポートできません。また、コンポーネント自体が型の情報を持ってしまうので、責務が分離できていない状態です。

onSubmitにはvalidation済みのデータを渡すのが前提

React Hook FormではonSubmitを呼び出す前にバリデーション処理が実行されます。そのため、onSubmit内でバリデーション処理を行うのは不適切です。

valuesAsNumberはControllerだと使用できない

input numberで入力しても、zodでバリデーションをかけると値が文字列として認識されていることが分かります。valuesAsNumberは文字列をnumberに変えるためのオプションです。Controllerだと使用できないので、その場合はregisterを使った方が良いです。

enumで型定義する場合、共用体型を使う

TypeScriptではenumを使うことができます。しかし、enumには昔から問題があるので、型を定義する目的であれば共用体型を使用しましょう。

type Kind = "food" | "dailyNecessities"

ちなみにenumとは、複数の定数を一つにまとめて定義したり管理したりすることができるものです。もしenumを使う場合、以下のように疑似enumを定義しましょう。疑似enumを使うことで定数の一元管理もできて、ハードコーディングを防ぐことができます。

const Kind = {
  Food: "food",
  DailyNecessities: "dailyNecessities",
} as const;

type Kind = typeof Kind[keyof typeof Kind] // => type Kind = "food" | "dailyNecessities"

console.log(Kind.Food); // => food

モデルにimmutableの設定を書く

immerというjsのライブラリを使うことで、ミュータブルなコードをイミュータブルにしてくれるそうです。未検証なので、もし実装する際はやってみようと思います。

export class Product implements Omit<api.Product, "storageTemperature" | "kind"> {
  [immerable] = true;

  // 省略

setStateでprevを使わない場合、書かなくて良い

setStateでprevを使わない場合、書かなくて大丈夫です。

■before

setState((prev) => [])

■after

setState([])

目的に対してかなり複雑な型定義をしない

複雑な型を定義することで、可読性が下がるので、目的に対して本当にこんな複雑な型を定義すべきなのかを考えた方が良いです。

定義元を見に行かないとどんなデータが渡されていたり、検証されているか分からない場合、ベタ書きした方が良い。

定義元を見に行くのがめんどくさいので、そのような場合はベタ書きした方が良いです。

オブジェクトのプロパティの型を追加したい場合、交差型を使う

交差型とは、&演算子を使って表現する型です。交差型を使うことで、柔軟にオブジェクトのプロパティの型を追加できます。

type User = {
  name: string;
};

type UserWithAge = User & {
  age: number;
};

const user: UserWithAge = {
  name: "yuki",
  age: 25,
};

APIのレスポンスのプロパティの型が必須になっていない場合、undefinedと型推論されてしまう

APIのレスポンスのプロパティの型が必須になっていない場合、undefinedと型推論されてしまいます。そのため、APIドキュメントを修正して必須にしておきましょう。そうすることで、undefinedを考慮せずに済みます。

setStateなどのセッターをpropsで渡す実装は避けた方が良い

コンポーネントで定義したsetStateなどのセッターを、子コンポーネントにpropsで渡す実装は避けた方が良いです。

■NG

// 子コンポーネント
export const SearchCandidates: FC<Props> = ({
  products,
  setProducts,
  setProduct,
}) => {

NGな理由

  • そもそもコンポーネントは自分自身にしか興味がないです。それなのになんで親コンポーネントからsetStateを受け取って、それで親コンポーネントのstateを更新してやらなきゃいけないのか?setStateを使ってどのようにstateを更新するかは親コンポーネントが持つべき情報であって、子コンポーネントが持つべきではないです。適切な場所に適切なコードを書くことによって、コンポーネント同士が密結合にならずに独立性を担保できます。独立性を担保することで、責務が適切に分離できていて変更に強くなったり、可読性やコンポーネントの再利用性が上がります。
  • useStateを定義したコンポーネント内で、どのようにstateが更新されるのかを把握することができないです。子コンポーネントを見に行かないといけないので、面倒です。

■OK

export const SearchCandidates: FC<Props> = ({
  products,
  onSelected,
}) => {

OKな理由

コンポーネントを切る際、コンポーネント名のディレクトリを作成してその配下にindex.tsxを作成する

コンポーネントを切る方法は2つ存在します。

  1. コンポーネント名.tsxを作成する
  2. コンポーネント名のディレクトリ/index.tsxを作成する

2番の方法はコンポーネント名のディレクトリにそのコンポーネントのテストをまとめることができるので、何かと嬉しいです。インポートする際もindex.tsxなので、パスを変えずにインポートできます(つまり、どちらも同じパスでインポートできます)。コンポーネントを切る場合は、どちらかの方法に統一しましょう。

カスタムフックで何でもかんでもまとめすぎない

そもそもカスタムフックとは、コンポーネントからロジックを抽出して再利用可能にした関数です。以下のuseProductsのようなカスタムフックの場合、名前から想像してAPIリクエストを発行してproductsおよびloadingを返すと考えられます。しかし、このuseProductsの返却値としてonSubmitやdisabledなど、使う側が想定できない値を返すのは良くありません。そして、onSubmitやdisabledなどはそのコンポーネントの都合で定義するものであって、カスタムフックに含めてしまうと、カスタムフックがそのコンポーネントに依存してしまい、カスタムフック自体の再利用性が落ちます。適切にカスタムフックを切りつつ、コンポーネントの都合で定義するものはコンポーネント内に定義しましょう。コンポーネントからロジックを分離しようとして、カスタムフックで何でもかんでもまとめるのではなく、①再利用性があるのか、②使う側にとって自然か、違和感がないかを考えてカスタムフックを作成しましょう。

■before

  const { products, disabled, onSubmit } = useProducts();

■after

  const { products } = useProducts();

  const disabled = // 省略
  const onSubmit = // 省略

カスタムフックを呼び出す際にonSuccessを渡すのではなく、カスタムフックが返すmutate関数を呼び出す際にonSuccessを渡すようにする

カスタムフックを呼び出す際にonSuccessを渡すのはやめましょう。メソッドを呼び出す際にonSuccessやonErrorを指定する方が違和感がないためです。また、現状、使う人が想定できないインターフェースになっています。「なぜカスタムフックを呼び出す際にonSuccessを渡さないといけないのか?」を定義元を見ないと分からず、使いづらいです。useMutationのmutate関数は呼び出す際にonSuccessやonErrorを指定できるようになっています。そのため、mutate関数を呼び出す際にonSuccessやonErrorを指定すればOKです。

■before

type Props = {
  onSuccess?: (data: PostProducts201Response) => void;
  onError?: () => void;
}

export const useCreateProduct = ({ onSuccess, onError }: Props) => {
  const api = // 省略

  const { mutate: create } = useMutation(
    (data: Data) => { return api.postProducts(data); },
    {
      onSuccess,
      onError,
    },
  );

  return {
    create,
  };
};

■after

export const useCreateProduct = () => {
  const api = // 省略

  const { mutate: create } = useMutation(
    (data: Data) => { return api.postProducts(data); },
  );

  return {
    create,
  };
};

カスタムフックのユースケースを制限するようなロジックをカスタムフックに含めない

ユースケースを制限するようなロジックは、大体カスタムフックを使う側の都合で定義されるロジックなので、そういうロジックはカスタムフックを使う側のコンポーネントに直接書いて、カスタムフックのユースケースを制限しないように気をつけましょう。

React Hook FormのvaluAsDateを利用して、Dateオブジェクトのインスタンスを作成する

valueAsDateを使うことで、ユーザーが入力した日付をDateオブジェクトのインスタンスに変換できます。APIリクエスト出す際にDateオブジェクトのインスタンスに自前で変換するのではなく、valueAsDateでユーザーが入力した瞬間に自動で変換するようにしましょう。

コンポーネント内で考慮すべきではないロジックを子コンポーネントに含めない

コンポーネント内で考慮すべきではないロジック(親コンポーネントの都合で決まるロジックなど)を子コンポーネントに含めると、子コンポーネントと親コンポーネントが依存してしまい、子コンポーネントの再利用性が下がります。そして、親コンポーネントが持つべき情報を子コンポーネントが持ってしまっているので、責務が正しく分離されていません。責務が正しく分離されていないと、子コンポーネント内で複雑なロジックを描く必要性も出てくるので、責務は適切に分離した方が良いです。

関数で1行の処理で実行結果だけをreturnする場合、アローの隣にリターンする値を書く

■before

  const { mutate: create } = useMutation(
    (data: Data) => { return api.postProducts(data); },
  );

■after

  const { mutate: create } = useMutation(
    (data: Data) => api.postProducts(data)
  );

APIから取得したデータをそのまま使うのではなく、フロント側で定義したモデルを使ってインスタンスを作成する

そもそもモデルとはなんなのかwikipediaで見てみましょう。

そのアプリケーションが扱う領域のデータと手続き(ビジネスロジック - ショッピングの合計額や送料を計算するなど)を表現する要素である。また、データの変更をビューに通知するのもモデルの責任である(モデルの変更を通知するのにObserver パターンが用いられることもある)。

フロントエンドのアプリケーションでAPIから取得したデータをそのまま使うと柔軟性が低いです。フロントエンドのアプリケーションなのにデータがAPIの型に依存しています。フロントエンドではフロントエンドに特化したデータの持ち方をしたいので、フロントエンドでもモデルを使用します。このモデルも基本的にはAPIの型に準拠するのですが、モデルを作ることによって、「データ」と「データに関する操作」をまとめられます。そしてフロントエンドに特化したデータの持ち方もできます(プロパティを増やしたり)。モデルを使わない場合、無秩序に色々な場所に「データ」と「データに関する操作」が定義されていて管理しづらいかつ変更に弱いです。

以下ではAPIのレスポンスをモデルを通してインスタンス化しています。こうすることで、フロントエンドのアプリケーションでデータとデータに関する操作を一元管理できます。

■before

  const { data, isLoading } = useQuery(["products"], () => api.getProducts());

■after

  const { isLoading } = useQuery(["products"], () => api.getProducts(), {
    onSuccess: ({ data: { products } }) => {
      setProducts(products.map((product) => new Product(product)));
    }
  });

↓ フロントエンドにおけるモデル

export class Product implements api.Product {
  [immerable] = true;

  id = "";
  name = "";
  nameEn = "";
  sku = "";

  constructor(data: Partial<Product> = {}) {
    Object.assign(this, data);
  }
}

フォームの値をそのまま投げたい

onSubmit関数の中で、フォームの値を整形して、APIリクエスト出すのはめんどくさいかつ複雑になってしまうので、onSubmitの中でフォームの値をなるべく整形しないでAPIリクエスト出せるようにInput要素にvalueAsNumberをつけたり、zodのtransformで整形しましょう。

複数のスプレッド構文を使っている場合、スプレッド構文の順番に気をつける

スプレッド構文の順番が違うだけで、プロパティの値が変わるので気をつけましょう。

const originalUser = { name: "yuki" };
const defaultUser = { name: "hoge" };

const userA = {
  ...originalUser,
  ...defaultUser,
};
// => { "name": "hoge" } 

const userB = {
  ...defaultUser,
  ...originalUser,
}; 
// => { "name": "yuki" }

bool値の変数の命名に気をつける

純粋に ~ かどうかを判定したい場合、is~で良いですが、~を表示したいかどうかを制御する場合は、show~の方が使う側がわかりやすいです。使う側にとって意味が分かりやすい命名をつけましょう。

NaNはnumber型

NaNはnumber型です。そのため、zodで以下のようにnumberとNaNのunion型にしても、priceはnumber型として考慮されます(型エイリアスでも同様です)。input numberで未入力を許容したいかつ値をnumber型として扱いたい場合、numberとNaNのunion型にすれば挙動を満たせるのでオススメです。初見の人には分かりづらいので、もしする場合はコメントを残しておくと良いでしょう。

↓ 型エイリアス

type Price = number | typeof NaN; // => type Price = number
// NOTE: 8個の商品で8個未満の商品を登録するために、デフォルト値のNaNを許容している
price: z.union([z.number().positive(), z.nan()]),

オブジェクトをスプレッド構文でpropsとして展開するのをやめる

props名が変数名に引きづられてしまい、props名を独自に定義するときに面倒なので、普通に書きます。普通に書いた方が自然です。

■before

<Product
  {...{name, age}}
/>

■after

<Product
  name={name}
  age={age}
/>

mutate関数の引数をオブジェクトリテラルでひとまとめにしない

関数を使う際に、引数をオブジェクトリテラルで囲むのは面倒であり、自然ではないです。なぜオブジェクトリテラルで囲むのかを定義元をみないといけないので、それも面倒です。

Propsにアロー関数を渡さない

Propsにアロー関数を渡さない方が良いのは、公式で推奨されています。

レンダー内でアロー関数を利用するとコンポーネントがレンダーされるたびに新しい関数が作成されるため、厳密な一致による比較に基づいた最適化が動作しなくなる可能性があります。

Propsにアロー関数を渡すことで、親コンポーネントが再レンダリングされた際にアロー関数が再生成されてしまい、その結果、propsが異なるので子コンポーネントが再レンダリングしてしまいます。不必要に子コンポーネントが再レンダリングしてしまうので、パフォーマンスが悪いです。Propsにはアロー関数を渡さず、事前に useCallback などを使用して関数を定義して、その関数を渡すようにしましょう。

return ( 
  <Todo
    onSelected={(item: Item) => setState( // 省略 )}
  />
)
const onSelected = useCallback((item: Item) => setState( // 省略 ), [])
return ( 
  <Todo
    onSelected={onSelected}
  />
)

終わり

単一責任の原則について調べていたら、wikipediaに「責務とは変更する理由である」と書いていて、すごくしっくりきたなと感じました。その言葉を知ってから、「なぜセッターを渡さなかったら責務が分離できるのか」を考えるとすごくしっくりくるなと感じました。

参考記事

コンポーネント(コンポーネントウェア)とは何か?コンポーネント指向や、その元になった「分割統治法」や「単一責任の原則」も含めて解説 | Promapedia(プロマペディア)

イラストで理解するSOLID原則 - Qiita

gapの余白指定が便利! gridとflexでできる新しいCSSレイアウト手法 - ICS MEDIA

useFieldArray | React Hook Form - Simple React forms validation

Zod | Documentation

TypescriptのEnum型の代わりにUnion型を使用する

イミュータブルが大事な理由、そしてImmerで簡単実現!

Classes | Immer

単一責任の原則 - Wikipedia

Reactのベストプラクティスを模索する - Qiita

JavaScript | Dateオブジェクトのインスタンスを作成する

独自フックの作成 – React

クラスを使わないアプリを実際に作ってみた気付き

React Hooksとカスタムフックが実現する世界 - ロジックの分離と再利用性の向上 - Qiita

コンポーネントに関数を渡す – React

SOLID原則で考えるReact設計

極端なパターンを考えて、技術的な落とし所を探っていく - mizdra's blog