0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React Hooks を Hackしよう!【Part28(番外編): Rules of Reactを理解して、より堅牢なコードを書こう!(3/3)】

Posted at

⚠️ 「なぜかフックが動かない...」その原因、呼び出す場所が問題かも?

Part1では「純粋性」と「冪等性」について、Part2では「コンポーネントやフックを呼び出すのはReact」というルールを学びました。Part3では、最も基本的かつ重要な「フックのルール(Rules of Hooks)」について詳しく解説します。

「条件分岐の中でフックを使うとなぜダメなのか?」「イベントハンドラ内でフックを呼べないのはなぜ?」——facebook/reactリポジトリの実際のエラーメッセージを見ながら、これらの疑問に答えていきます!

1. フックのルールとは?

フック(Hooks) は、JavaScript関数として定義されていますが、呼び出せる場所に制限がある特別な種類の再利用可能なUIロジックです。

名前が use で始まる関数は、Reactではフックと呼ばれます。

フックのルール一覧

ルール 説明
フックはトップレベルでのみ呼び出す ループ、条件分岐、ネストされた関数の中で呼び出さない
フックはReact関数からのみ呼び出す 通常のJavaScript関数からは呼び出さない

なぜこれらのルールが必要なのか?

Reactは、フックが毎回同じ順序で呼び出されることを前提として動作しています。この順序に依存して、各フックの状態を正しく追跡しています。ルールを破ると、Reactはどのフックがどの状態に対応するかを判断できなくなります。

2. フックはトップレベルでのみ呼び出す

ループ、条件分岐、ネストされた関数、try/catch/finallyブロックの中でフックを呼び出してはいけません。代わりに、常にReact関数のトップレベルで、早期リターンより前にフックを使用してください。

✅ 正しい呼び出し方

フックは、Reactが関数コンポーネントをレンダリングしている間のみ呼び出すことができます:

function Counter() {
  // ✅ Good: 関数コンポーネントのトップレベル
  const [count, setCount] = useState(0);
  // ...
}

function useWindowWidth() {
  // ✅ Good: カスタムフックのトップレベル
  const [width, setWidth] = useState(window.innerWidth);
  // ...
}

❌ フックを呼び出してはいけない場所

以下のケースでは、use で始まる関数(フック)を呼び出すことはサポートされていません:

❌ NG パターン 説明
条件分岐やループの中 呼び出し順序が変わる可能性がある
条件付きreturn文の後 条件によって呼び出されない可能性がある
イベントハンドラ内 レンダリング外で呼び出される
クラスコンポーネント内 関数コンポーネント専用
useMemo、useReducer、useEffectに渡す関数内 コールバック関数内では使用不可
try/catch/finallyブロック内 エラーハンドリングの流れで呼び出し順序が変わる

3. 各パターンの詳細とReact Compilerによる検出

3.1 条件分岐内でのフック呼び出し

function Bad({ cond }) {
  if (cond) {
    // 🔴 Bad: 条件分岐の中(修正するには外に出す!)
    const theme = useContext(ThemeContext);
  }
  // ...
}

React Compilerのエラー

facebook/reactリポジトリのcompiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-if-consequent.expect.mdから:

Error: Hooks must always be called in a consistent order, and may not be 
called conditionally. See the Rules of Hooks 
(https://react.dev/warnings/invalid-hook-call-warning)

  2 |   let x = null;
  3 |   if (props.cond) {
> 4 |     x = useHook();
    |         ^^^^^^^ Hooks must always be called in a consistent order, 
                      and may not be called conditionally.

✅ 正しい方法

function Good({ cond }) {
  // ✅ Good: トップレベルで呼び出す
  const theme = useContext(ThemeContext);
  
  if (cond) {
    // themeを使った処理
  }
  // ...
}

3.2 ループ内でのフック呼び出し

function Bad() {
  for (let i = 0; i < 10; i++) {
    // 🔴 Bad: ループの中(修正するには外に出す!)
    const theme = useContext(ThemeContext);
  }
  // ...
}

React Compilerのエラー

facebook/reactリポジトリのcompiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-for.expect.mdから:

Error: Hooks must always be called in a consistent order, and may not be 
called conditionally. See the Rules of Hooks 
(https://react.dev/warnings/invalid-hook-call-warning)

  2 |   let i = 0;
  3 |   for (let x = 0; useHook(x) < 10; useHook(i), x++) {
> 4 |     i += useHook(x);
    |          ^^^^^^^ Hooks must always be called in a consistent order, 
                       and may not be called conditionally.

✅ 正しい方法

function Good() {
  // ✅ Good: トップレベルで呼び出す
  const theme = useContext(ThemeContext);
  
  for (let i = 0; i < 10; i++) {
    // themeを使った処理
  }
  // ...
}

3.3 条件付きreturn文の後でのフック呼び出し

function Bad({ cond }) {
  if (cond) {
    return;
  }
  // 🔴 Bad: 条件付きreturnの後(修正するにはreturnの前に移動!)
  const theme = useContext(ThemeContext);
  // ...
}

React Compilerのエラー

facebook/reactリポジトリのcompiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-after-early-return.expect.mdから:

Error: Hooks must always be called in a consistent order, and may not be 
called conditionally. See the Rules of Hooks 
(https://react.dev/warnings/invalid-hook-call-warning)

  3 |     return null;
  4 |   }
> 5 |   return useHook();
    |          ^^^^^^^ Hooks must always be called in a consistent order, 
                       and may not be called conditionally.

✅ 正しい方法

function Good({ cond }) {
  // ✅ Good: 早期リターンの前に呼び出す
  const theme = useContext(ThemeContext);
  
  if (cond) {
    return null;
  }
  
  return <div style={{ color: theme.primary }}>Content</div>;
}

3.4 イベントハンドラ内でのフック呼び出し

function Bad() {
  function handleClick() {
    // 🔴 Bad: イベントハンドラの中(修正するには外に出す!)
    const theme = useContext(ThemeContext);
  }
  // ...
}

イベントハンドラはレンダリング中に実行されるのではなく、ユーザーの操作に応じて実行されます。そのため、フックの呼び出し順序を保証できません。

✅ 正しい方法

function Good() {
  // ✅ Good: トップレベルで呼び出す
  const theme = useContext(ThemeContext);
  
  function handleClick() {
    // themeを使った処理
    console.log(theme.primary);
  }
  
  return <button onClick={handleClick}>Click me</button>;
}

3.5 useMemo、useReducer、useEffectに渡す関数内でのフック呼び出し

function Bad() {
  const style = useMemo(() => {
    // 🔴 Bad: useMemoの中(修正するには外に出す!)
    const theme = useContext(ThemeContext);
    return createStyle(theme);
  });
  // ...
}

React Compilerのエラー

facebook/reactリポジトリのcompiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-function-expression-object-expression.expect.mdから:

Error: Hooks must be called at the top level in the body of a function 
component or custom hook, and may not be called within function expressions. 
See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)

Cannot call hook within a function expression.

   8 |           const y = {
   9 |             inner() {
> 10 |               return useFoo();
     |                      ^^^^^^ Hooks must be called at the top level 
                            in the body of a function component or custom hook

✅ 正しい方法

function Good() {
  // ✅ Good: トップレベルで呼び出す
  const theme = useContext(ThemeContext);
  
  const style = useMemo(() => {
    return createStyle(theme);
  }, [theme]);
  // ...
}

3.6 クラスコンポーネント内でのフック呼び出し

class Bad extends React.Component {
  render() {
    // 🔴 Bad: クラスコンポーネントの中
    // (修正するにはクラスの代わりに関数コンポーネントを書く!)
    useEffect(() => {})
    // ...
  }
}

フックは関数コンポーネント専用です。クラスコンポーネントでは使用できません。

✅ 正しい方法

// ✅ Good: 関数コンポーネントを使用
function Good() {
  useEffect(() => {
    // 副作用の処理
  });
  
  return <div>Content</div>;
}

3.7 try/catch/finallyブロック内でのフック呼び出し

function Bad() {
  try {
    // 🔴 Bad: tryブロックの中(修正するには外に出す!)
    const [x, setX] = useState(0);
  } catch {
    const [x, setX] = useState(1);
  }
}

try/catch/finallyブロック内でフックを呼び出すと、エラーが発生した場合にフックの呼び出し順序が変わる可能性があります。

✅ 正しい方法

function Good() {
  // ✅ Good: トップレベルで呼び出す
  const [x, setX] = useState(0);
  
  try {
    // xを使った処理(エラーが発生する可能性のある処理)
  } catch (error) {
    // エラーハンドリング
    console.error(error);
  }
  
  return <div>{x}</div>;
}

4. eslint-plugin-react-hooksでルール違反を検出

これらのルール違反は、eslint-plugin-react-hooksプラグインを使用して検出できます。

インストール

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

# yarn
yarn add eslint-plugin-react-hooks --dev

設定(Flat Config)

// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';

export default defineConfig([
  reactHooks.configs.flat.recommended,
]);

eslint-plugin-react-hooksについて

facebook/reactリポジトリにはeslint-plugin-react-hooksが含まれており、以下のルールを静的解析で検出できます:

ルール 説明
rules-of-hooks フックのルールを検証(エラー)
exhaustive-deps 依存配列の完全性を検証(警告)

5. フックはReact関数からのみ呼び出す

通常のJavaScript関数からフックを呼び出してはいけません。代わりに:

  • ✅ React関数コンポーネントからフックを呼び出す
  • ✅ カスタムフックからフックを呼び出す

このルールに従うことで、コンポーネント内のすべてのステートフルなロジックが、そのソースコードから明確に見えるようになります。

✅ 正しい例

function FriendList() {
  const [onlineStatus, setOnlineStatus] = useOnlineStatus(); // ✅
  return <div>{onlineStatus}</div>;
}

❌ 悪い例

function setOnlineStatus() { // ❌ コンポーネントでもカスタムフックでもない!
  const [onlineStatus, setOnlineStatus] = useOnlineStatus();
}

カスタムフックについて

カスタムフックは他のフックを呼び出すことができます(それがカスタムフックの目的です)。これが機能するのは、カスタムフックも関数コンポーネントのレンダリング中にのみ呼び出されることが想定されているためです。

6. なぜフックの呼び出し順序が重要なのか?

Reactは、フックが呼び出される順序に基づいて、各フックの状態を追跡しています。

内部での動作イメージ

function Form() {
  // 1番目のフック呼び出し
  const [name, setName] = useState('Alice');    // → 状態スロット #1
  
  // 2番目のフック呼び出し
  const [age, setAge] = useState(25);           // → 状態スロット #2
  
  // 3番目のフック呼び出し
  useEffect(() => {                             // → エフェクトスロット #3
    document.title = name;
  }, [name]);
  
  // ...
}

毎回のレンダリングで、Reactは同じ順序でフックが呼び出されることを期待しています:

レンダリング1: useState → useState → useEffect
レンダリング2: useState → useState → useEffect  ✅ 同じ順序
レンダリング3: useState → useState → useEffect  ✅ 同じ順序

条件分岐でフックを呼び出すと...

function BadForm({ showAge }) {
  const [name, setName] = useState('Alice');    // 常に1番目
  
  if (showAge) {
    const [age, setAge] = useState(25);         // 条件によって存在しない!
  }
  
  useEffect(() => {                             // 2番目 or 3番目?
    document.title = name;
  }, [name]);
}
レンダリング1 (showAge=true):  useState → useState → useEffect
レンダリング2 (showAge=false): useState → useEffect            ❌ 順序が変わった!

Reactは「2番目のuseStateがなくなった」ことを検知できず、useEffectを2番目のuseStateと混同してしまいます。これがバグの原因になります。

7. まとめ

ルール ✅ 正しい ❌ 間違い
トップレベルで呼び出す 関数コンポーネント/カスタムフックの本体 条件分岐、ループ、ネスト関数
早期リターンの前で呼び出す if の前でフック呼び出し 条件付き return の後
React関数から呼び出す 関数コンポーネント、カスタムフック 通常のJavaScript関数
コールバック外で呼び出す トップレベルで呼び出し useMemo/useEffect内

Rules of Reactの全体像

Part1: コンポーネントとHooksは純粋でなければならない

  • 純粋性の重要性
  • 冪等性
  • 副作用の分離
  • ローカルミューテーション
  • PropsとStateの不変性
  • HooksとJSXの不変性

Part2: コンポーネントやフックを呼び出すのはReact

  • コンポーネント関数を直接呼び出さない
  • フックを通常の値として取り回さない
  • フックを動的に変更しない
  • フックを動的に使用しない

Part3: フックのルール(Rules of Hooks)

  • フックはトップレベルでのみ呼び出す
  • フックはReact関数からのみ呼び出す
  • 呼び出し順序が重要な理由

これらのルールを守ることで、React Compilerによる自動最適化が可能になり、予測可能で保守しやすいReactアプリケーションを構築できます!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?