Reactの純粋関数とは?コンポーネントを純粋に保つルールとESLintの検出範囲
Reactのコンポーネントとフックは、純粋関数として書くことが前提になっています。純粋関数とは、同じ入力を与えれば常に同じ結果を返し、呼び出される前から存在していた値を書き換えない関数のことです。この前提が崩れると、開発中は動いていた画面が本番のビルド設定やReact Compilerの最適化を挟んだ瞬間に壊れます。ここでは公式が定義する3条件を確認したうえで、違反コードの直し方と、ESLintがどこまで自動で捕まえてくれるのか(そしてどこを見逃すのか)を実測値で示します。
まとめ
- 純粋関数は「同じ入力なら同じ出力」「呼び出し前から存在する値を書き換えない」の2点で定義されます。
- react.devがコンポーネントとフックに課すのは、冪等であること・レンダー中に副作用を持たないこと・ローカル以外の値を変更しないことの3条件です。
- 違反はまず開発環境で表面化します。Strict Modeはコンポーネント関数の本体に加え、
useStateの初期化関数・set関数・useMemo・useReducerに渡した関数を追加で1回実行し、Effectとrefコールバックはセットアップとクリーンアップを1周多く走らせるため、レンダーのたびに結果が変わるコードはそこでズレます。 - React Compilerは「ルール違反かもしれない」と判断したコードの最適化を降ります。検出できない違反があると、逆に壊れたまま最適化されます。
- eslint-plugin-react-hooks 7.1.1 は
Math.random()やpropsの書き換えを止めますが、new Date()とcrypto.randomUUID()は素通りします。ESLintだけを純粋性の担保にはできません。
以下、定義・症状・修正・検出範囲の順に見ていきます。
純粋関数の定義|同じ入力に同じ出力を返し、外部を書き換えない関数
純粋関数の判定は2点だけです。引数が同じなら戻り値が同じか。関数の外にある値を書き換えていないか。特定の言語やフレームワークに属する用語ではなく、JavaScriptで書くと違いは一目で分かります。
// 純粋:引数だけで結果が決まり、外側には触れない
function add(a, b) {
return a + b;
}
// 不純:呼ぶたびに結果が変わり、外側のtotalを書き換えている
let total = 0;
function addToTotal(n) {
total += n;
return total;
}
addToTotal(1) は1回目に1、2回目に2を返します。引数は同じなのに戻り値が違う時点で、この関数の結果はキャッシュできず、呼ぶ順番にも依存します。Reactが嫌うのはまさにこの性質です。
この2条件が揃うと、関数呼び出しをその戻り値で置き換えてもプログラムの意味が変わらなくなります。参照透過性と呼ばれる性質で、実利は3つあります。結果をメモ化しても安全なこと、実行順序や実行回数に依存しないこと、テストが引数と戻り値の対応だけで書けることです。Reactが最適化の前提に純粋性を置いているのは、1つ目と2つ目をフレームワーク側で使いたいからです。
Reactが「純粋」と呼ぶ3条件
react.devの「Components and Hooks must be pure」は、純粋なコンポーネント/フックを次の3条件で定義しています。
- 冪等(Idempotent):同じ入力(コンポーネントならprops・state・context、フックなら引数)で実行すれば、毎回同じ結果になること。
- レンダー中に副作用がない(Has no side effects in render):副作用を伴う処理はレンダーとは別に、イベントハンドラかEffectで実行すること。
- 非ローカルな値を変更しない(Does not mutate non-local values):レンダー中に作られたのではない値を、コンポーネントやフックが書き換えないこと。
3つ目の条件が「一切ミューテーション禁止」ではない点は押さえておく価値があります。禁止されているのは非ローカルな値の変更で、レンダー中に自分で作った配列やオブジェクトへの書き込みは公式も明確に許可しています。境界は後述します。
純粋性が壊れたときに現れる症状
Strict Modeの二重呼び出しとハイドレーション不一致
純粋性の違反は、たいてい「たまに表示がおかしい」という形で現れます。原因を特定しにくいのは、レンダーが何回走るかがReactの都合で決まるからです。react.devは「Rendering can happen at any time」と述べたうえで、コンポーネントは互いのレンダー順序に依存してはならない(”components should not depend on each others’ rendering sequence”)と明記しています。
これを開発中に炙り出す仕掛けがStrict Modeです。公式の説明は「By calling the component functions twice, Strict Mode helps find components that break these rules.」——開発環境ではコンポーネント関数を意図的に2回呼び、1回目と2回目で結果が変わるコンポーネントを浮かび上がらせます。本番ビルドでは二重呼び出しは行われないため(”Strict Mode has no effect in production, so it won’t slow down the app for your users.”)、パフォーマンス上の懸念で外す必要はありません。二重描画がうるさいからとStrict Modeを外すのは、検知器の電池を抜く行為です。
サーバーサイドレンダリングを使っている場合、症状はハイドレーションエラーとして出ます。サーバーで生成したHTMLとクライアントの初回レンダー結果が食い違うと警告になりますが、レンダー中に new Date() や Math.random() を呼んでいれば、サーバーとクライアントで値が一致するはずがありません。ハイドレーション不一致の調査で犯人が見つからないときは、レンダー内の非冪等な呼び出しを疑うのが近道です。
React Compilerの最適化スキップとランタイム不具合
純粋性がコードレビューの作法から実利の問題に変わったのは、React Compilerが安定版になってからです(babel-plugin-react-compiler 1.0.0、2025年10月7日公開)。React Compilerによる自動メモ化は、コンポーネントが純粋であることを前提に useMemo や useCallback 相当の処理を機械的に挿入します。
ここでコンパイラの判断は2通りに分かれます。公式のデバッグガイドいわく「When it encounters code that might break these rules, it safely skips optimization rather than risk changing your app’s behavior.」——ルールを破りそうなコードを見つけると、動作を変えるリスクを避けて最適化そのものを降ります。つまり違反を書いた分だけ、そのコンポーネントは最適化の恩恵を受けられません。
厄介なのはもう一方です。公式は不具合の典型として「your code violates the Rules of React in subtle ways that the compiler couldn’t detect, and the compiler mistakenly compiled a component it should have skipped」を挙げています。検出をすり抜けた違反があると、本来スキップすべきコンポーネントが最適化されてしまい、実行時に挙動が変わります。コンパイラを入れた直後に「特定の画面だけ値が更新されない」といった症状が出たら、まず疑うべきはコンパイラのバグではなく自分のコードの純粋性違反です。
レンダー中の違反コードと書き換え方
モジュールスコープ変数の再代入と修正パターン
以下の違反例と修正例は、react.dev「Components and Hooks must be pure」および「Keeping Components Pure」が挙げている公式の例に沿っています。もっとも多い違反が、コンポーネントの外に置いた変数をレンダー中に書き換えるパターンです。
let guest = 0;
function Cup() {
guest = guest + 1; // NG:レンダーのたびに外の変数が増える
return <h2>Tea cup for guest #{guest}</h2>;
}
このコンポーネントは呼ぶたびに違うJSXを返します。修正は単純で、変わる値を引数(props)として受け取る形にします。
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>;
}
同じ理屈で、レンダー中の document.title = ... のようなDOM書き換えも違反です。ページタイトルの更新はEffectに移します。ただしReact 19以降は、コンポーネントの中に <title>{draft.title}</title> をそのまま書けばReactが <head> へ引き上げるため、タイトルやメタタグの更新にEffectを使う必要はなくなりました。React 19で追加されたドキュメントメタデータのサポートがこれにあたります。
propsの直接書き換えと回避策
propsは読み取り専用です。受け取ったオブジェクトのプロパティを書き換えると、親が持っている値まで変わってしまいます。
function PostNG({ item }) {
item.url = new URL(item.url, base); // NG:propsを直接書き換えている
return <Link url={item.url}>{item.title}</Link>;
}
function PostOK({ item }) {
const url = new URL(item.url, base); // OK:コピーを作る
return <Link url={url}>{item.title}</Link>;
}
stateの更新場所と直接代入の誤り
stateの書き換えも同じく禁止ですが、直し方には注意点があります。useState が返す count は const なので count = count + 1 はそもそも実行時エラーになり、セッター setCount 経由でなければReactは更新を検知できません。ただしsetCount はイベントハンドラやEffectの中で呼ぶものであって、レンダー中に呼んではいけません。レンダー中の呼び出しは再レンダーを繰り返す無限ループの原因になり、後述の react-hooks/set-state-in-render が検出対象にしています。
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1); // NG:レンダー中のsetState
return <button>{count}</button>;
}
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => setCount(count + 1); // OK:操作を起点に更新する
return <button onClick={handleClick}>{count}</button>;
}
JSXへ渡した後のオブジェクト書き換え
見落としやすいのがこの3つ目です。JSXを作った後にその中身のオブジェクトを書き換えると、どちらの値でレンダーされるかはReactのスケジュール次第になります。
function Page({ colour }) {
const styles = { colour, size: "large" };
const header = <Header styles={styles} />;
styles.size = "small"; // NG:stylesはすでにJSXへ渡した後
const footer = <Footer styles={styles} />;
return <>{header}<Content />{footer}</>;
}
意図どおりに動かすには、共有せず別々の値を作ります。ヘッダー用とフッター用でオブジェクトを分ければ、あとから書き換える必要そのものが消えます。
function Page({ colour }) {
const headerStyles = { colour, size: "large" };
const header = <Header styles={headerStyles} />;
const footerStyles = { colour, size: "small" }; // OK:別のオブジェクトを作る
const footer = <Footer styles={footerStyles} />;
return <>{header}<Content />{footer}</>;
}
ローカルミューテーションが許される境界
「Reactでは配列を書き換えてはいけない」と覚えている人は多いのですが、これは行き過ぎです。react.devはこれをローカルミューテーション(local mutation)と呼び、「it’s completely fine to change variables and objects that you’ve just created while rendering.」と明言しています。判定基準は、その値がレンダー中に作られたかどうかだけです。
function FriendList({ friends }) {
const items = []; // OK:レンダー中に作った配列
for (let i = 0; i < friends.length; i++) {
items.push(<Friend key={friends[i].id} friend={friends[i]} />);
}
return <section>{items}</section>;
}
items は毎回作り直されるので、書き換えは次のレンダーに持ち越されません。逆に同じ const items = [] をコンポーネントの外に出した瞬間、書き込みが蓄積して違反になります。ループ内で配列を組み立てる書き方を無理にスプレッド構文へ書き換える必要はありません。
副作用を置く場所の判断基準
そもそも副作用とは、呼び出し元に値を返すこと以外に、外から観測できる影響を及ぼす処理のことです。外部の状態を読む、書き換える、画面やネットワークに働きかける処理はすべて該当します。
置き場所は、ほとんどの場合イベントハンドラで足ります。react.devも「In React, side effects usually belong inside event handlers.」とし、ハンドラはレンダー中に実行されないので純粋である必要はない(”event handlers don’t need to be pure”)と補足しています。
useEffectは順位が下です。公式は「If you’ve exhausted all options – and only as a last resort – you can also handle side effects using useEffect.」と述べており、ユーザー操作に紐づけられる処理をEffectに書くのは避けるべきとされています。判断の順番は次のようになります。
function SaveButton({ draft }) {
// 1. ユーザー操作が起点 → イベントハンドラ
const handleClick = () => {
fetch("/api/drafts", { method: "POST", body: JSON.stringify(draft) });
};
// 2. 外部システムとの同期が目的 → Effect
useEffect(() => {
document.title = draft.title;
}, [draft.title]);
return <button onClick={handleClick}>保存</button>;
}
「操作が起点ならハンドラ、外部との同期ならEffect、それ以外はレンダー中に書いてよい純粋な計算」と切り分けると、useEffectの数はかなり減ります。
フックの引数と返り値を書き換えない理由
純粋性のルールはコンポーネントだけでなくフックにも同じようにかかります。カスタムフックが受け取った引数を書き換えると、react.devの表現では「the custom hook’s memoization will become incorrect」——メモ化が効かなくなるのではなく、メモ化の結果が不正になります。入力が変わっていないとReactが判断して、更新されるべき箇所に古い値が返ります。
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
const newIcon = { ...icon }; // OK:コピーしてから変更する
if (icon.enabled) {
newIcon.className = computeStyle(icon, theme);
}
return newIcon;
}
フック自身が返した値も、受け取った側で書き換えてはいけません。返り値は次のレンダーで再利用される可能性があるためです。呼び出しの順序や条件分岐に関する制約は別系統のルール(フックのルール)で、こちらはReactフックの一覧と使い分けで整理しています。
ESLintで検出できる純粋性違反と素通りする違反
flat.recommendedで有効になる16ルール
純粋性のチェックは、eslint-plugin-react-hooks 7系(7.1.1、2026年4月17日公開)が担当します。長らくこのプラグインは rules-of-hooks と exhaustive-deps の2ルールだけでしたが、6.1.0 でReact Compiler由来の静的解析ルールが recommended-latest に入り、7.0.0 でプリセットが recommended と recommended-latest の2つに整理されたうえで、コンパイラルールが既定で有効になりました。6.0.0 は誤ってリリースされ直後に取り下げられた版なので、移行先として指定しないでください。
// eslint.config.js(フラットコンフィグ)
import reactHooks from "eslint-plugin-react-hooks";
export default [reactHooks.configs.flat.recommended];
この configs.flat.recommended を読み込むと、7.1.1では16ルールが有効になります(recommended-latest は void-use-memo を加えた17ルール)。純粋性に直接効くのは次の4本です。
| ルール | 止める書き方 |
|---|---|
| react-hooks/purity | レンダー中の非冪等な関数呼び出し |
| react-hooks/immutability | props・フック引数・DOMの書き換え |
| react-hooks/globals | モジュールスコープ変数の再代入 |
| react-hooks/set-state-in-render | レンダー中のsetState呼び出し |
残りは refs、static-components、error-boundaries、set-state-in-effect、use-memo、preserve-manual-memoization、incompatible-library、unsupported-syntax、config、gating と、従来の2本です。なお configs.recommended(フラットでない方)は .eslintrc 形式なので、フラットコンフィグにそのまま置くと読み込みに失敗します。configs.flat.recommended を指すのが正解です。
検出されたコードと素通りしたコード
ESLint 10.8.0 と eslint-plugin-react-hooks 7.1.1(flat.recommended)で、代表的な違反を検査した結果が下記です。
function A() { const r = Math.random(); } // error react-hooks/purity
function B() { const t = Date.now(); } // error react-hooks/purity
function C() { const t = performance.now(); } // error react-hooks/purity
function D() { setN(n + 1); } // error react-hooks/set-state-in-render
function E() { document.title = "x"; } // error react-hooks/immutability
function F({ item }) { item.url = "x"; } // error react-hooks/immutability
function G() { guest = guest + 1; } // error react-hooks/globals
メッセージも具体的です。react-hooks/purity は「Cannot call impure function during render」、react-hooks/immutability は「This value cannot be modified」に続けて「Modifying component props or hook arguments is not allowed. Consider using a local variable instead.」と、修正方針まで提示します。
問題は次の側です。同じ設定で、これらは1件も報告されませんでした。
function H() { const d = new Date(); } // 検出なし
function I() { const r = crypto.randomUUID(); } // 検出なし
function J() { const v = localStorage.getItem("k"); } // 検出なし
function K() { console.log("render"); } // 検出なし
new Date() はreact.dev自身が違反例として挙げている書き方ですが、ルールは反応しません。Date.now() が止まって new Date() が通るという非対称からは、任意の不純さを推論しているのではなく、既知の関数呼び出しを個別に判定していると考えられます。crypto.randomUUID() も同じ扱いで、キー生成をレンダー内でやってしまう典型的な事故は検出されません。
ここが結論です。ESLintは純粋性の下限を守るツールであって、純粋性の証明にはなりません。導入すべきなのは当然として、それだけで安心するとハイドレーション不一致とCompiler由来の実行時バグが残ります。Strict Modeを有効にしたまま開発し、レンダー関数の中に「現在時刻・乱数・外部ストレージの読み取り」が入っていないかは人間の目で確認する体制が要ります。コードベース全体の設定漏れを洗うなら、React Doctorによる診断を併用すると設定側の抜けを拾えます。
よくある質問
純粋関数とは何ですか?
同じ引数を渡せば常に同じ値を返し、関数の外にある状態を変更しない関数です。数学の関数と同じ性質だと考えると分かりやすく、結果がキャッシュでき、実行順序に依存せず、テストが引数と戻り値だけで書けるという利点があります。Reactではこの性質をコンポーネントとフックにも要求します。
レンダー中のconsole.logは書いてもよいですか?
厳密には外部への書き込みなので副作用ですが、アプリの表示結果には影響しないため、デバッグ目的なら実用上の問題はありません。eslint-plugin-react-hooks 7.1.1でも検出されません。ただしStrict Modeの二重呼び出しでログが2回出るため、ログの回数からレンダー回数を数えるのは誤りです。2回目のログは薄い文字で表示され、React DevToolsの設定(既定はオフ)で完全に抑制することもできます。
new Date()はESLintで検出されますか?
検出されません。ESLint 10.8.0+eslint-plugin-react-hooks 7.1.1の flat.recommended では、Date.now() と Math.random() は react-hooks/purity で止まりますが、new Date() と crypto.randomUUID() は報告されません。現在時刻をレンダー中に取得したい場合は、useState(() => new Date()) のように遅延初期化で初回レンダーの値に固定するか、Effect側で更新する形に書き換えます。useState(new Date()) と書くと引数が毎レンダー評価されてしまい、レンダー中の不純な呼び出しは解消しません。なおStrict Modeでは初期化関数自体も開発時に2回呼ばれますが、保持される値は1つです。
C++の純粋仮想関数と純粋関数は同じものですか?
別物です。C++の純粋仮想関数(pure virtual function)は virtual void draw() = 0; のように宣言だけを書き、派生クラスに実装を強制する言語機能で、副作用の有無とは関係ありません。純粋抽象関数という呼び方も同じものを指します。またHaskellなどの純粋関数型言語は、純粋関数を計算の基本単位に据えた言語処理系の分類であり、JavaScriptで純粋関数を書くことがそのまま純粋関数型プログラミングになるわけではありません。
Strict Modeを無効にすれば二重描画は止まりますか?
止まりますが、勧められません。二重呼び出しは開発環境限定の挙動で、本番ビルドには影響しないためパフォーマンス上の理由で外す必要がありません。二重描画で表示が崩れるということは、そのコンポーネントが純粋でない証拠です。Strict Modeを外すと、症状だけが隠れて原因は残ります。
React Compilerを使わないなら純粋性は気にしなくてよいですか?
いいえ。Reactは以前からレンダーを中断・再実行する前提で設計されており、純粋でないコンポーネントはSuspenseやトランジションを使った時点で表示がずれます。サーバーサイドレンダリング環境ではハイドレーション不一致にもなります。Compilerはそれを可視化したにすぎません。