✍️

【React】Custom Hooksは描画の材料をstateから計算するために使う

2022/12/01に公開

みなさんは普段custom hooksを設計する時にどんなことを考えてますか?
私は結構感覚に頼って設計していることが多く、明確な考えを持ち合わせていませんでした。

custom hooksのことを説明する過程で、普段どのように設計しているか言語化する必要に迫られました。曖昧な考えを明確にしておくことで、一歩前に設計スキルを進めたいなと思い筆を執りました。🙆‍♀️

結論

custom hooksは描画の材料をstateから計算するために使う。
ここで、描画の材料というのは簡単にJSXに埋め込めるデータぐらいの意味合いで便宜的に利用している。

普段から「このhooksはどのようなstateに関連しているのか?」と自問して、保守性の高いhooksを作っていきたい💪


さて、custom hooksの設計について考えるために、

  1. そもそもcustom hooksは何か?
  2. そして何を解決しているのかを考える必要があります。
  3. そしてreactの責務と比較してcustom hooksの責務を考えていきます。

そもそもcustom hooksとは?

hooksはただの関数です。
useというprefixがついた名前を持ち、関数型コンポーネント内で同じ順番で必ず1回実行される必要があります。
そしてユーザーが作成したhooksがcustom hooksと呼ばれています。

custom hooksが解決した課題は?

公式によると、そもそも導入されたきっかけは関数型コンポーネントでstatefulなロジックを共通化するためでした。
関数型コンポーネントの実態はJSXを返すただの関数であるため、複数回実行した時にstateを保持することができなかったのです。

結果的に、hooksはクラスコンポーネントでもできなかったライフサイクルについて横断的なロジックを共通化することが可能になりました。

reactが解決した課題は?

reactはプログラム(外界を含む)とユーザー間のやりとりを宣言的に書くために作られました。
複数のコンポーネントによってUIを構成していますので、コンポーネントの責務について考えてみましょう。

コンポーネントの描画は2段階に分けることができると思います。

  1. stateから描画の材料を計算する(what)
  2. 描画の材料からJSXへ描画する(how)

ここで描画の材料と表現していますが、これは便宜的なもので簡単な変換のみでJSXへと変換できる状態のデータぐらいの意味合いで使っています。
はたしてどちらがcustom hooksの責務でしょうか?

custom hooksの責務は、描画の材料を計算することである。

ユーザーへ提示されるview部分はJSXによって決定されます。なので、custom hooksの責務は1番の描画の材料を計算する方にありそうです。
また、custom hooksで重要な要素はstateを利用することです。なぜならstateが関連しない変換であればhooksにせずに関数で事足りるからです。
ですので、custom hooksはstatefulに描画の材料を計算することを責務に持つと考えることができるでしょう。

描画の段階を源流の方から確認していきます。
まずはじめにstateを操作するhooksを軽くおさらいしておきましょう。

stateを操作するhooksたちについて(描画の源流)

state自身を操作するhooksについて分類していきます。これらのhooksを用いて、さまざまなcustom hooksを作ることができますので、大変重要です。
useStateのようにstate・更新関数を操作できるhooksと、stateに依存した操作のできるhooksに大別することができます。

  1. state・更新関数を操作できるhooks(What)
    1. useState
    2. useReducer
      • 更新関数がより自由に設定できるuseState
    3. useContext
      • コンポーネントの壁を超えたuseState
    4. useRef
      • reactのライフサイクルから外れた値。再レンダーされない
  2. stateに依存した操作のできるhooks(When)
    1. useEffect
      • 外界やstateに依存した関数の実行
    2. useMemo
      • stateに依存した(あるいは全く依存しない)オブジェクトを返す
    3. useCallback
      • stateに依存した(あるいは全く依存しない)関数を返す

2番のstateに依存した操作のできるhooksに対しては、依存するstate・値を配列の形で提供します。hooksの内部でその配列の変化を検知し、それに応じて関数の実行や新たなオブジェクトの計算を実行しています。

render hooks(描画の材料はどこまでか?)

続いてstateから描画の材料へ変換する部分を見ていきます。
描画の材料をどこまでと捉えるかで、hooksの幅が広がります。

従来のcustom hooksでは描画する対象である文字列・真偽値等のデータを計算するのが自然ですが、当然最後のJSXまで描画してしまってもよいです。完成品までお膳立てしてあげましょう。このパターンにはrender hooksという名前がついています。[1]


例とともにrender hooksを説明したいと思います。render hooksについて熟知されている方は飛ばしていただいて構いません👍
例えばRadio Buttonを実装するところを想像してみましょう。どのように実装すればよいでしょうか?

Radio Buttonが保持する必要のあるstateは何でしょうか?複数の選択肢の内何を選択しているかを保持しておく必要がありますね。

Radio Button (シンプルなコンポーネント)

従来のコンポーネントの形式だと、stateとstatelessなコンポーネントを一緒に使うことになります。stateとコンポーネントの結合を疎にすることができます。

type ConvenienceStore = 'FamilyMart' | 'SevenEleven' | 'Lawson';
const convenienceStores = ['FamilyMart', 'SevenEleven', 'Lawson'];

const RadioButton = <T,>({ value, onChange, isSelected }: {
  value: T;
  onChange: (value: string) => void;
  isSelected: boolean;
}): React.ReactElement => {
  // ただラジオボタンを描画するだけなので省略
};

const SomeComponent = (): React.ReactElement => {
  const [selectedConvenienceStore, setSelectedConvenienceStore] = useState<ConvenienceStore>('FamilyMart');
  return (
    <>
      {convenienceStores.map((convenienceStore, index) => {
        return (
          <RadioButton
            key={index}
            value={convenienceStore}
            onChange={setConvenienceStore}
            isSelected={selectedConvenienceStore === convenienceStore}
          />
        );
      })}
    </>
  );
};

Radio Button (render hooks)

render hooks式ですと、Radio Buttonの描画まで完了させる事ができます。
どちらも一長一短ですが、今回のようなRadio Buttonの場合は汎用的で凝集性が高いことが多いので、個人的にはrender hooksで実装するほうが好き。

type ConvenienceStore = 'FamilyMart' | 'SevenEleven' | 'Lawson';
const convenienceStores = ['FamilyMart', 'SevenEleven', 'Lawson'];

const RadioButton = <T,>({ value, onChange, isSelected }: {
  value: T;
  onChange: (value: string) => void;
  isSelected: boolean;
}): React.ReactElement => {
  // ただラジオボタンを描画するだけなので省略
};

const useRadioButtons = <T,>(choices: T[], initialChoice: T): [T, React.ReactElement] => {
  const [selectedChoice, setSelectedChoice] = useState<T>(initialChoice);
  return [
    selectedChoice,
    <>
      {convenienceStores.map((convenienceStore, index) => {
        return (
          <RadioButton
            key={index}
            value={convenienceStore}
            onChange={setConvenienceStore}
            isSelected={selectedConvenienceStore === convenienceStore}
          />
        );
      })}
    </>
    // 描画済みのJSXを返してあげている
  ]
};

const SomeComponent = (): React.ReactElement => {
  const [selectedConvenienceStore, radios] = useRadioButtons<ConvenienceStore>(convenienceStores, 'FamilyMart'); // 汎用的に使えるように、選択肢の型をジェネリクスにして実装
  return (
    <>
      {radios}
    </>
  );
};

Container / Presentational Pattern

余談として、custom hooksの責務に関連した設計にContainer / Presentational Patternというものが存在します。
ロジックのみを担当するコンポーネントとUIのみを担当するコンポーネントを分離することで、ロジックとUIの関心を分離しましょう、という設計です。

hooksが存在していることで、ロジックを共通化することができるようになりました。ではContainer / Presentational Patternは不要になったのでしょうか?

Storybook等でUIをテストするならば、このPatternで分離しておくことでUIのテストが格段にしやすくなります。なぜならpropsを見繕うだけで複数のstateをシミュレートできるからです。
分離しStorybookを導入するコストとリターンが釣り合うかは要件によりますが、非エンジニアとのコミュニケーションが多い場合、Storybookの導入コストを下げる工夫をすれば、ペイすることが多いでしょう(この部分はあまり経験がなく確固たる考えを持ち合わせていないです)

まとめ

以上のように、custom hooksの利点がstateから描画の材料の計算をまとめられるところにあるというのがわかりました。

今後は、custom hooksに関連してテスト方法についても自分の言葉で説明できるようにしておきたいです。

余談ですが、今までその場の感覚でcustom hooksを作成していたので、言語化したら何か発見があるか?とワクワクしながら進めていたのですが、
実際に言葉にしてみると至極当たり前に感じられて少し残念に思ってます。

脚注
  1. render hooksについて記述のあるサイト。おそらくこちらが初出。 ↩︎

Discussion