📝

Reactコンポーネントの「普通こう書くよね」は、実は当たり前じゃない

に公開
2026/02/21
4件

はじめに

Reactでコンポーネントを書くとき、こんなふうに書きますよね。

function Greeting({ name }) {
  return <p>こんにちは、{name}さん!</p>;
}

propsを受け取って、JSXを返す。「普通こう書くよね」と、特に意識せずにやっていると思います。

でも、この「普通」の裏側には、Reactが全力で信頼しているルールがあります。

それが 純粋性(Purity) です。

このルールがあるからこそ、React.memoは安全にレンダリングをスキップできるし、Server Componentsは複数ユーザーのリクエストを同時に捌けるし、Concurrent Modeはレンダリングを途中で捨てられる。

普段なんとなく守れているかもしれない。でも、意識しないまま破ってしまうこともある。今回は、この 「知らない間に使っているけど、めちゃくちゃ重要なルール」 の話です。

純粋性って何?

ルール自体はシンプルです。

  1. 同じ入力には、同じ出力を返す
  2. スコープ外の変数を変更(ミューテート)しない

この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

しぐれ煮しぐれ煮

Next.jsのServer Componentsを使っている方は多いと思います。サーバー側でコンポーネントをレンダリングしてHTMLを返す仕組みですね。

ではないです。HTMLではなくJSONを返す仕組みです。また、サーバを使用しません。

1
あさひあさひ

ご指摘ありがとうございます。

おっしゃる通り、Server Componentsが生成するのはHTMLではなくRSC Payload(React Server Component Payload)で、HTMLへの変換はSSRが担当する別のステップですね。「Server ComponentsがレンダリングしてHTMLを返す」という表現はこの2つを混同していました。記事の該当箇所を修正します。

サーバの使用についても、React仕様としてはビルド時実行も可能なので「サーバ側で」という表現が不正確な点、理解しました。記事ではNext.jsの文脈で書いていましたが、より正確な表現に改めます。

Server Componentsの内部的な仕組みについてはまだ理解が浅く、これから深掘りしていきたいと思っていたところなので、リンクも含めとても勉強になりました。ありがとうございます。

1
しぐれ煮しぐれ煮

この2つを満たす関数を 「純関数」 と呼びます。そしてReactは、すべてのコンポーネントが純関数であるという前提で動いています。

外部のファイルやデータベースの値を読み取るといった「実行する度に値が変わる可能性のある関数」についてはどう考えますか?

https://ja.react.dev/reference/rsc/server-components

これは公式の資料に書かれていたサンプルのコードです。

import db from './database';

async function Note({id}) {
  // NOTE: loads *during* render.
  const note = await db.notes.get(id);
  return (
    <div>
      <Author id={note.authorId} />
      <p>{note}</p>
    </div>
  );
}

async function Author({id}) {
  // NOTE: loads *after* Note,
  // but is fast if data is co-located.
  const author = await db.authors.get(id);
  return <span>By: {author.name}</span>;
}

この関数たちは以下の2つを満たしていません。

  • 同じ入力には、同じ出力を返す
  • スコープ外の変数を変更(ミューテート)しない

これはダメな例に該当しますか?

あさひあさひ

コメントありがとうございます。

ご指摘の通り、Server Componentsでデータベースを読むコンポーネントは厳密な意味での純関数ではありません。同じidを渡しても、DBの値が更新されれば出力が変わります。

この記事はReact公式のKeeping Components Pureをベースにしており、クライアント側のレンダリングサイクルにおける純粋性の話です。React.memoのスキップやConcurrent Modeの中断が安全に動くために「同じ入力→同じ出力」が必要になる、という文脈です。

Server Componentsはサーバー上で1リクエストにつき1回実行され、クライアント側のレンダリングサイクルには参加しないため、ここでの純粋性の議論とは適用範囲が異なるかと考えています。

本件のデータ取得と純粋性の関係を含め、Server Componentsについては、それだけで記事にできるだけの興味深いテーマだと思っているので、別の記事で改めて扱う予定です。

記事中にスコープの補足がなくわかりづらかった点は改善します。ありがとうございます。

1