React Hooksの掟と舞台裏


React 16.8でついに正式版となったReact Hooksですが、使うにあたっていくつかの掟があります。そして、その掟がどのような理由で存在するのかについても解説していきます。

React Hooksの掟

  1. Hooks関数を関数コンポーネントやCustom Hooksの外から呼んではならない
  2. Hooks関数の呼び出しは、関数コンポーネントやCustom Hooksのトップレベルに置かなければならない
  3. ある関数コンポーネントについて、Hooks関数の呼び出しは毎回同じ順序・回数でなければならない

掟の存在理由

クラスコンポーネントであれば、明確なデータの保管場所として「インスタンス」がありますし、前にWeakMapへの保管をしてみたときも、保存用のWeakMapオブジェクトと、キーとなるオブジェクトが存在しました。では、そういったインスタンスとコード上で結びつかない関数コンポーネントの場合、Hooksのデータをどう管理すればいいのでしょうか。

実は、それはReact内部でのコンポーネント管理に組み込まれています。コンポーネントと関連付けたデータ領域が存在して、そこへHooksの実行に関して必要な情報が格納されていきます。

ここで、掟に従った関数呼び出しを行うことで、「単なる関数呼び出し」をHooksのデータ領域へ正しく結び付けられるようになります。つまり、1を破った「コンポーネント外での呼び出し」では対応付けるデータ領域が見つからずエラーとなって、2番目3番目を破って「一貫しないHooksの呼び出し」を行ってしまうと、どれとデータのヒモ付けをすればいいかわからなくなります1(開発版Reactではinvariantによるチェックが入っていて、エラーとなります)。

掟破りしそうな場面

早期リターン

条件を満たしていない場合、レンダリングが最低限の文言あるいはnullでいいので、早めにreturnしたくなることがあるかと思います。しかし、上記のように、Hooksの呼び出しパターンがコンポーネントについて一貫していないとエラーになりますので、「早期リターン」といっても、Hooksをすべてセットした「後」でなければなりません。

function SomeComponent({items}) {
  const [value, setValue] = useState(0);
  // if(!items) return null; // ここに書くとHooksの実行がずれる
  useEffect(() => /* 略 */);

  // Hooksのセットが完了してから
  if(!items) return null;
}

コレクションに対してuseEffect

コレクションの要素全てに何かを仕掛ける場合も、ループさせてuseEffect、というわけには行きません。逆に、useEffectでループさせる必要があります。

function AnotherComponent({collection}) {
  // これはできない!
  collection.forEach(item => useEffect(() => /* 略 */));

  // 中でループさせる
  useEffect(() => {
    collection.forEach(item => /* 略 */);
  });
}

その他

Hooksの含まれる関数コンポーネントを単なる関数として実行することも不可能です(Hooksをコンポーネントと無関係な場面で実行することとなってしまいます)。

ESLintによる支援

eslint-plugin-react-hooksというReactのプラグインがあって、この掟を守るためにも役立ちます。

Reactサイトへのリンク


  1. もちろん、理屈の上では制御構造に入っていても「同じ順序・同じ回数だけ」呼べばいいのでしょうけれど、基本的に制御構造は変化させるためにあるものなので、やめたほうがいいとは思います。