TL;DR

React のコンポーネントから呼び出したときに、特別な機能を提供する関数。コンポーネントと同じく純粋関数であるべきで、常に同じ順序で呼ぶ必要がある。

仕組み

呼び出す順序が大事な理由

React は、再レンダリングで useState が再び呼ばれたときにどの state を返すべきかを、呼び出しの順序で特定しているので、Hook が毎回同じ場所で呼び出されることに依存している。公式ドキュメントは次のように説明している。

React はすべてのコンポーネントに対して state のペアの配列を保持しています。
また、現在のペアインデックスも管理しており、レンダー前に 0 に設定されます。
useState が呼び出されるたびに、React は次の状態ペアを提供し、インデックスをインクリメントします。

簡易的な useState の実装は次のようになる。

// React 内部での useState の動作
function useState(initialState) {
  let pair = componentHooks[currentHookIndex]
  if (pair) {
    // 最初のレンダリングではないので、すでに state のペアが存在している。
    // それを返し、次のフック呼び出しに備える。
    currentHookIndex++
    return pair
  }
 
  // これは初めてのレンダリングなので、state のペアを作成し、それを保存する。
  pair = [initialState, setState]
 
  function setState(nextState) {
    // ユーザーが state の変更を要求したとき、新しい値をペアに設定する。
    pair[0] = nextState
    updateDOM()
  }
 
  // 今後のレンダリングのためにペアを保存し、次のフック呼び出しに備える。
  componentHooks[currentHookIndex] = pair
  currentHookIndex++
  return pair
}

Hook のルール

「Hook はトップレベルでのみ呼び出す」というルールを守れば、Hook は常に同じ順序で呼ばれる。逆に、条件分岐の中で Hook を呼ぶと順序が変わってしまう。

呼び出す順序さえ一貫していれば、Hook をコンポーネントから直接呼んでも、他の関数を通して間接的に呼んでもよい。ここから「Hook は React の関数からのみ呼び出す」というルールが導ける。

カスタムフック

他の Hook を呼び出す関数も、呼び出される順序を保つために Hook として扱う必要がある。このような Hook をカスタムフックと呼ぶ。慣例として、関数名に use という接頭辞を付ける。

例

スライドの番号を管理するカスタムフックの例である。

import { useState } from "react"
 
export const useSlideIndex = () => {
  const [slideIndex, setSlideIndex] = useState(0)
 
  return [slideIndex, setSlideIndex] as const
}

as const は、戻り値が readonly なタプルであることを示す。付けないと、TypeScript はより汎用的な型に推論するので、戻り値は (number | React.Dispatch<React.SetStateAction<number>>)[] になってしまう。

注意点

ESLint の eslint-plugin-react-hooks プラグインを入れると、Hook のルールに違反しないように検査してくれる。

npm install --save-dev eslint-plugin-react-hooks

2024 年 9 月時点では、このプラグインは Flat Config 向けの共有設定を提供していない。そのため、eslint.config.js には次のように自分でルールを読み込む。

import globals from "globals"
import pluginJs from "@eslint/js"
import tseslint from "typescript-eslint"
import pluginReact from "eslint-plugin-react"
import eslintConfigPrettier from "eslint-config-prettier"
import eslintPluginReactHooks from "eslint-plugin-react-hooks"
 
export default [
  { files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"], ignores: ["dist"] },
  { languageOptions: { globals: globals.browser } },
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
  {
    ...pluginReact.configs.flat.recommended,
    settings: { react: { version: "detect" } },
    plugins: {
      "react-hooks": eslintPluginReactHooks,
    },
    rules: {
      "react/react-in-jsx-scope": "off",
      ...eslintPluginReactHooks.configs.recommended.rules,
    },
  },
  eslintConfigPrettier,
]

関連

出典