🎯 「Reactが勝手にやってくれる」を信じていますか?
Part1では、純粋性・冪等性・副作用の分離といった「コンポーネントとHooksは純粋でなければならない」というルールを学びました。Part2では、もう一つの重要なルール「コンポーネントやフックを呼び出すのはReact」について深掘りします。
「なぜ直接コンポーネント関数を呼んではいけないのか?」「フックをpropsで渡すと何が問題なのか?」——これらの疑問に、facebook/reactリポジトリの実際のエラーメッセージを見ながら答えていきます!
1. コンポーネントやフックを呼び出すのはReact
ユーザー体験を最適化するために必要に応じてコンポーネントやフックを呼び出すというのはReact自身の責務です。
Reactは宣言型(declarative)です。あなたは何(what) をレンダーしたいのかだけをReactに伝え、それをどうやって(how) ユーザーにうまく表示するのかについてはReactが考えます。
このルールに含まれる4つの原則
| 原則 | 説明 |
|---|---|
| コンポーネント関数を直接呼び出さない | JSX内でのみコンポーネントを使用する |
| フックを通常の値として取り回さない | フックは常に関数として呼び出す |
| フックを動的に変更しない | 高階フックを作成しない |
| フックを動的に使用しない | フックをpropsとして渡さない |
なぜReactに制御を委ねるのか?
React Compilerのドキュメント compiler/docs/DESIGN_GOALS.md には以下のように記載されています:
React's rules exist to help developers build robust, scalable applications and form a contract that allows us to continue improving React without breaking applications. React Compiler depends on these rules to safely transform code, and violations of rules will therefore break React Compiler's optimizations.
Reactのルールは、堅牢でスケーラブルなアプリケーションを構築し、Reactが将来の改善を安全に行えるようにするための「契約」なのです。
2. コンポーネント関数を直接呼び出さない
コンポーネントはJSX内でのみ使用すべきです。通常の関数として呼び出してはいけません。呼び出すのはReactです。
❌ 悪い例 vs ✅ 良い例
function BlogPost() {
return <Layout><Article /></Layout>; // ✅ Good: JSX内でコンポーネントを使用
}
function BlogPost() {
return <Layout>{Article()}</Layout>; // 🔴 Bad: 直接関数として呼び出している
}
なぜダメなのか?
レンダー中にコンポーネント関数をいつ呼び出すかを決定する必要があるのはReactです。Reactでは、これをJSXを使用して行います。
コンポーネントにフックが含まれている場合、ループや条件内でコンポーネントを直接呼び出すと、フックのルールにいとも簡単に違反してしまいます。
// 🔴 Bad: ループ内でコンポーネントを直接呼び出す
function List({ items }) {
const results = [];
for (const item of items) {
results.push(ItemComponent(item)); // フックのルールに違反する可能性!
}
return <ul>{results}</ul>;
}
// ✅ Good: JSX内でコンポーネントを使用
function List({ items }) {
return (
<ul>
{items.map(item => (
<ItemComponent key={item.id} {...item} />
))}
</ul>
);
}
Reactにレンダーの指揮権を与えることの利点
Reactにレンダーの指揮権を与えることで、多くの利点が得られます:
| 利点 | 説明 |
|---|---|
| コンポーネントが単なる関数以上のものになる | フックを通じて、Reactはローカルstateなどの機能をコンポーネントに追加できる |
| コンポーネントの型情報を差分検出処理時に利用できる | ツリーの概念的構造についてReactはより多くの情報を得られる |
| Reactがユーザー体験を向上させられる | 大きなコンポーネントツリーの再レンダーがメインスレッドをブロックしないよう制御できる |
| より良いデバッグ体験 | コンポーネントを基本部品として認識し、リッチな開発者ツールを構築できる |
| より効率的な差分検出処理 | ツリー内のどのコンポーネントを再レンダーすべきか正確に把握できる |
ページ遷移の例
例えば <Feed> から <Profile> ページへとレンダーが移行するとき、Reactはそれらを再利用しようとせずに済みます。JSXを使用することで、Reactはこれらが異なるコンポーネントであることを認識できます。
3. フックを通常の値として取り回さない
フックはコンポーネントまたはフックの内部でのみ呼び出すべきです。通常の値のように取り回してはいけません。
フックは、コンポーネントにReactの機能を加えるために使用します。常に関数として呼び出すだけにし、通常の値のように取りまわしてはいけません。
ローカル・リーズニング(Local Reasoning)
これによりローカル・リーズニングが可能になります。つまり、開発者がそのコンポーネントだけを見てコンポーネントにできることをすべて理解できるようになります。
// ✅ Good: フックの呼び出しが明確
function UserProfile() {
const user = useUser();
const theme = useTheme();
return <div style={{ color: theme.primary }}>{user.name}</div>;
}
コンポーネントのコードを見れば、useUser と useTheme というフックが使われていることが一目瞭然です。
ルール違反時の影響
このルールを破ると、Reactはコンポーネントを自動的に最適化することができなくなります。React Compilerの最適化は、フックの呼び出しが静的で予測可能であることを前提としています。
4. フックを動的に変更しない
フックは可能な限り**「静的」**であるべきです。つまり、動的に変更してはいけません。例えば、高階(higher-order)フックを書くべきではありません。
❌ 高階フックの問題
function ChatInput() {
const useDataWithLogging = withLogging(useData); // 🔴 Bad: 高階フックを作成している
const data = useDataWithLogging();
}
✅ 正しい方法:静的なバージョンのフックを作成
function ChatInput() {
const data = useDataWithLogging(); // ✅ Good: 静的なフックを使用
}
function useDataWithLogging() {
const data = useData();
// ロギングロジックをここにインライン化
useEffect(() => {
console.log('Data fetched:', data);
}, [data]);
return data;
}
フックはイミュータブルであるべきで、動的に変更するべきではありません。フックを動的に変更する代わりに、望ましい機能を持つ静的なバージョンのフックを作成してください。
5. フックを動的に使用しない
フックを動的に使用してはいけません。例えば以下のように、フックそのものを値としてコンポーネントに渡して依存性注入を行わないようにしてください。
❌ フックをpropsとして渡す問題
function ChatInput() {
return <Button useData={useDataWithLogging} /> // 🔴 Bad: フックをpropsとして渡している
}
React Compilerによる検出
facebook/reactリポジトリの compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-as-prop.expect.md には、このパターンがエラーとして検出される例が示されています:
// Input
function Component({useFoo}) {
useFoo();
}
// Error
Error: Hooks must be the same function on every render, but this value may
change over time to a different function.
See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks
✅ 正しい方法:フックをコンポーネント内で直接呼び出す
function ChatInput() {
return <Button />
}
function Button() {
const data = useDataWithLogging(); // ✅ Good: フックを直接使用
return <button>{data.label}</button>;
}
function useDataWithLogging() {
// フックの動作を変更するための条件付きロジックは
// フック内にインライン化する
const data = useData();
// ...
return data;
}
こうすることで <Button /> を理解しデバッグするのがずっと簡単になります。
フックを動的に使用することの問題点
フックを動的に使用すると:
- アプリの複雑さが大幅に増す
- ローカル・リーズニングを妨げる
- 長期的にはチームの生産性を低下させる
- 条件付きでフックを呼び出すルール違反を誘発しやすい
テストのためにコンポーネントをモックする必要がある場合
フックを注入する代わりに、サーバをモックして固定データで応答する方が良いでしょう。可能であれば、end-to-endテストでアプリをテストする方が通常はより効果的です。
6. React Compilerによるルール検証
React Compilerは、これらのルールに違反したコードを検出し、エラーを報告します。
条件付きフック呼び出しの検出
// Input
function Component(props) {
if (props.cond) {
return null;
}
return useHook();
}
// Error
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)
ネストされた関数内でのフック呼び出しの検出
// Input
function ComponentWithHookInsideCallback() {
useEffect(() => {
useHookInsideCallback(); // 🔴 コールバック内でフックを呼んではいけない
});
}
// Error
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.
動的フックの検出
// Input
function Component() {
const someFunction = useContext(FooContext);
const useOhItsNamedLikeAHookNow = someFunction;
useOhItsNamedLikeAHookNow(); // 🔴 動的にフックを使用
}
// Error
Error: Hooks must be the same function on every render, but this value may
change over time to a different function.
7. eslint-plugin-react-hooksの活用
facebook/reactリポジトリには 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,
]);
利用可能なルール
| ルール | 説明 |
|---|---|
rules-of-hooks |
フックのルールを検証(エラー) |
exhaustive-deps |
依存配列の完全性を検証(警告) |
purity |
純粋性のルールを検証 |
immutability |
不変性のルールを検証 |
React Compiler ルールの有効化
最新のReact Compilerルールを試したい場合は、recommended-latest プリセットを使用してください:
export default defineConfig([
reactHooks.configs.flat['recommended-latest'],
]);
8. まとめ
| ルール | 説明 | 理由 |
|---|---|---|
| コンポーネント関数を直接呼び出さない | JSX内でのみ使用 | Reactがレンダリングを最適化できる |
| フックを通常の値として取り回さない | 関数として呼び出すだけ | ローカル・リーズニングを可能にする |
| フックを動的に変更しない | 高階フックを書かない | 静的なバージョンを作成する |
| フックを動的に使用しない | propsとして渡さない | 複雑さを減らし、ルール違反を防ぐ |
Part1: コンポーネントとHooksは純粋でなければならない
- 純粋性の重要性
- 冪等性
- 副作用の分離
- ローカルミューテーション
- PropsとStateの不変性
- HooksとJSXの不変性
Part2: コンポーネントやフックを呼び出すのはReact
- コンポーネント関数を直接呼び出さない
- フックを通常の値として取り回さない
- フックを動的に変更しない
- フックを動的に使用しない
これらのルールを守ることで、React Compilerによる自動最適化が可能になり、より高速で保守しやすいReactアプリケーションを構築できます!