Reactコンポーネントの「普通こう書くよね」は、実は当たり前じゃない
はじめに
Reactでコンポーネントを書くとき、こんなふうに書きますよね。
function Greeting({ name }) {
return <p>こんにちは、{name}さん!</p>;
}
propsを受け取って、JSXを返す。「普通こう書くよね」と、特に意識せずにやっていると思います。
でも、この「普通」の裏側には、Reactが全力で信頼しているルールがあります。
それが 純粋性(Purity) です。
このルールがあるからこそ、React.memoは安全にレンダリングをスキップできるし、Server Componentsは複数ユーザーのリクエストを同時に捌けるし、Concurrent Modeはレンダリングを途中で捨てられる。
普段なんとなく守れているかもしれない。でも、意識しないまま破ってしまうこともある。今回は、この 「知らない間に使っているけど、めちゃくちゃ重要なルール」 の話です。
純粋性って何?
ルール自体はシンプルです。
- 同じ入力には、同じ出力を返す
- スコープ外の変数を変更(ミューテート)しない
この2つを満たす関数を 「純関数」 と呼びます。そしてReactは、すべてのコンポーネントが純関数であるという前提で動いています。
具体的に見てみましょう。
ダメな例:純粋じゃないコンポーネント
let likeCount = 0;
function LikeButton() {
likeCount += 1; // スコープ外の変数を書き換えている
return <button>♡ {likeCount}</button>;
}
このコンポーネントは呼ぶたびに結果が変わります。<LikeButton />を3回並べたら ♡ 1、♡ 2、♡ 3。同じ入力(propsなし)なのに出力が毎回違う。これは純粋じゃありません。
良い例:純粋なコンポーネント
function LikeButton({ count }) {
return <button>♡ {count}</button>;
}
count={5} を渡せば必ず ♡ 5 が返る。何回呼んでも同じ。これが純粋です。
ちなみに、レンダリング中にその場で作った変数を書き換えるのはOKです(ローカルミューテーション)。外に影響を与えないからですね。
function LikeButtons() {
const buttons = [];
for (let i = 1; i <= 5; i++) {
buttons.push(<LikeButton key={i} count={i} />); // buttons はこの関数内で作ったもの → OK
}
return buttons;
}
「いつどこで誰が呼び出したとしても、必ず同じ結果になる。」
これが純粋性のキモです。
本題:この「当たり前」がないと何が壊れるのか
「ルールはわかった。で、これがそんなに大事なの?」
大事です。Reactの主要な機能は、この純粋性の上に成り立っています。具体的に見ていきましょう。
1. メモ化が「安全に」できる
React.memoは「propsが前回と同じなら、レンダリングをスキップして前の結果を使い回す」仕組みです。
これが成り立つのは、コンポーネントが純粋だからです。
- 純粋 → 同じ入力なら必ず同じ出力
- つまり、propsが変わっていなければ結果も絶対に同じ
- だから計算をスキップしても安全
(厳密にはReactの「入力」にはprops・state・contextの3つがありますが、stateやcontextが変わればどのみち再レンダリングしなきゃいけないので、React.memoはpropsだけ考慮すれば十分です)
もし純粋じゃなかったらどうなるか。
function Greeting({ name }) {
const hour = new Date().getHours();
if (hour < 12) {
return <p>おはよう、{name}さん!</p>;
}
return <p>こんにちは、{name}さん!</p>;
}
propsの name は同じでも、時間が変われば結果が変わります。React.memoでスキップしたら、朝に表示した「おはよう」が昼になっても更新されない。キャッシュが信用できなくなるわけです。
純粋だからこそ「入力が同じ=結果も同じ」と言い切れて、安全にサボれる。これがメモ化の土台です。
2. サーバーで安全に使い回せる
Next.jsのServer Componentsを使っている方は多いと思います。Server Componentsはサーバー側でコンポーネントをレンダリングし、その結果をRSC Payload(React Server Component Payload)としてクライアントに送信する仕組みです。
サーバーでは複数のユーザーからのリクエストが同時に処理されます。もしコンポーネントが純粋じゃなかったら——
let requestCount = 0;
function Page() {
requestCount += 1;
return <div>リクエスト #{requestCount}</div>;
}
ユーザーAのリクエストで #1、ユーザーBで #2。他人のリクエストに干渉してしまいます。
純粋なコンポーネントなら、同じpropsに対して常に同じ結果を返すので、誰のリクエストで実行しても安全。コンポーネント同士が干渉する心配がありません。
普段Next.jsでServer Componentsを当たり前に使えているのは、この純粋性が前提にあるからです。
3. レンダリングを途中で破棄できる
Reactには「Concurrent Mode」という仕組みがあります。簡単に言うと、レンダリングの途中で「あ、新しいデータが来た。今の計算もう古いから捨てよう」ができる仕組みです。
これも純粋性あってこそです。
純粋なコンポーネントはレンダリング中に外部に一切影響を与えません。だから途中まで計算した結果を捨てても、 「何も起きなかったのと同じ」 になります。
純粋だからこそ、安全に中断・やり直しができるわけです。
まとめ
Reactの純粋性、振り返ってみましょう。
| Reactの機能 | 純粋性がないとどうなる? |
|---|---|
| React.memo | キャッシュが信用できない |
| Server Components | ユーザー間でリクエストが干渉する |
| Concurrent Mode | 途中で中断したら副作用が残る |
「コンポーネントは純粋に書きましょう」——これはただのお作法じゃありません。Reactは純粋性を前提にすることで、キャッシュも、サーバーでの実行も、レンダリングの中断も、安全かつ効率的にやれるようにしています。
普段「普通こう書くよね」でやっているその書き方が、実はReactの強さを支えている。「当たり前」の中身を知ると、Reactの設計がちょっと好きになるかもしれません。
Discussion
ではないです。HTMLではなくJSONを返す仕組みです。また、サーバを使用しません。
ご指摘ありがとうございます。
おっしゃる通り、Server Componentsが生成するのはHTMLではなくRSC Payload(React Server Component Payload)で、HTMLへの変換はSSRが担当する別のステップですね。「Server ComponentsがレンダリングしてHTMLを返す」という表現はこの2つを混同していました。記事の該当箇所を修正します。
サーバの使用についても、React仕様としてはビルド時実行も可能なので「サーバ側で」という表現が不正確な点、理解しました。記事ではNext.jsの文脈で書いていましたが、より正確な表現に改めます。
Server Componentsの内部的な仕組みについてはまだ理解が浅く、これから深掘りしていきたいと思っていたところなので、リンクも含めとても勉強になりました。ありがとうございます。
外部のファイルやデータベースの値を読み取るといった「実行する度に値が変わる可能性のある関数」についてはどう考えますか?
これは公式の資料に書かれていたサンプルのコードです。
この関数たちは以下の2つを満たしていません。
これはダメな例に該当しますか?
コメントありがとうございます。
ご指摘の通り、Server Componentsでデータベースを読むコンポーネントは厳密な意味での純関数ではありません。同じidを渡しても、DBの値が更新されれば出力が変わります。
この記事はReact公式のKeeping Components Pureをベースにしており、クライアント側のレンダリングサイクルにおける純粋性の話です。React.memoのスキップやConcurrent Modeの中断が安全に動くために「同じ入力→同じ出力」が必要になる、という文脈です。
Server Componentsはサーバー上で1リクエストにつき1回実行され、クライアント側のレンダリングサイクルには参加しないため、ここでの純粋性の議論とは適用範囲が異なるかと考えています。
本件のデータ取得と純粋性の関係を含め、Server Componentsについては、それだけで記事にできるだけの興味深いテーマだと思っているので、別の記事で改めて扱う予定です。
記事中にスコープの補足がなくわかりづらかった点は改善します。ありがとうございます。