本文へスキップ
再レンダリングとは何か

Part 7 · レンダリングの仕組み

40 / 64

再レンダリングとは何か

何が描き直しを起こし、どこまで広がるのか

Part 4 の「useState」 で、setCount は画面を直接書き換えるのではなく、関数をもう一度実行させると説明しました。

この章では、その「もう一度実行する」という動きを目で見える形にします。

きっかけは、いまのところ 2 つです。自分の state が変わったときと、親がもう一度描き直されたとき。 (3 つめは Part 9 の「Context」で出てきます)

大切なのは、props が変わったからではないことです。 親が描き直されれば、props を受け取っていない子も描き直されます。

React では、コンポーネントの関数が実行されることを再レンダリングと呼びます。 これが起きても、画面のすべてが一から作り直されるわけではありません。

再レンダリングとは、関数がもう一度実行されること

React のコンポーネントは関数です。 state を変えると、その関数がまた呼ばれます。 これが 再レンダリング です。

再レンダリングのカウンター

押すたびにコンポーネント関数がもう一度走る

このコンポーネントは 0 回ボタンを押されました。

上のデモでボタンを押すと、カードの見出しの横にある数字が増えます。 これはこのコンポーネントの関数が、 もう一度実行された回数です。

render-counter.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";

export function RenderCounter() {
  const [count, setCount] = useState(0);
  useTrackDemoRender();

  return (
    <div className="flex flex-col gap-4">
      <p>このコンポーネントは {count} 回ボタンを押されました。</p>
      <Button size="sm" onClick={() => setCount((current) => current + 1)}>
        再レンダリングする
      </Button>
    </div>
  );
}

再レンダリングは、画面の更新ではない

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <p onClick={() => setCount((current) => current + 1)}>
      {count}
    </p>
  );
}

setCount は React への依頼です。 「値が変わったよ」と教えます。 React はそのあと、コンポーネントの関数をもう一度実行します。

ここで「結果」とは何かをはっきりさせておきます。 これが分かると、この先の Part 8 が一気に読みやすくなります。

Part 1 でやったとおり、JSX はただの値でした。<button>{count}</button> を実行すると、 画面が書き換わるのではなく「こういう button を出してくれ」という指示書が 1 つできあがります。

// 実行すると、だいたいこういう形の値になる
{ type: "button", props: { children: 1 } }

関数がもう一度実行されると、この指示書がもう 1 枚できます。 React がやっているのは、前回の指示書と今回の指示書を見比べることです。

前回: { type: "p", props: { children: 0 } }
今回: { type: "p", props: { children: 1 } }
         ↑ タグは同じ。中の文字だけ違う

違うのは文字だけなので、文字だけ書き換えます。要素そのものは作り直しません。だからボタンを押していても フォーカスが外れませんし、入力欄の中身も消えません。

この「見比べ」を差分の検出と呼びます。 Part 3 の key は、 まさにこの見比べのときに使われる目印でした。

render-counter.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";

export function RenderCounter() {
  const [count, setCount] = useState(0);
  useTrackDemoRender();

  return (
    <div className="flex flex-col gap-4">
      <p>このコンポーネントは {count} 回ボタンを押されました。</p>
      <Button size="sm" onClick={() => setCount((current) => current + 1)}>
        再レンダリングする
      </Button>
    </div>
  );
}

子が props を受け取っていなくても再レンダリングされる

下のデモでは、子コンポーネントに props を渡していません。 それでも親のボタンを押すと、子の render 回数が増えます。

親の更新で子も再レンダリング

親の state を更新すると、子も再び実行される

親のカウント: 0

子コンポーネント

この子は props を受け取っていません。

React は子の関数をもう一度実行して、その結果を比べます。 props が同じでも、親が再び描き直されるときは子も動きます。

parent-re-renders-child.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";

function Child() {
  useTrackDemoRender();

  return (
    <div className="rounded-xl border border-slate-200 bg-slate-50 p-4 text-sm">
      <p className="font-semibold">子コンポーネント</p>
      <p>この子は props を受け取っていません。</p>
    </div>
  );
}

export function ParentRerendersChild() {
  const [count, setCount] = useState(0);

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-xl border border-slate-200 bg-slate-50 p-4">
        <p>親のカウント: {count}</p>
        <Button size="sm" onClick={() => setCount((current) => current + 1)}>
          親を更新する
        </Button>
      </div>
      <Child />
    </div>
  );
}

props の変化はきっかけではない

function Child() {
  useTrackDemoRender();
  return <p>私は props を受け取っていません。</p>;
}

function Parent() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount((current) => current + 1)}>
        親を更新する
      </button>
      <Child />
    </div>
  );
}

この例では、子が受け取る値はありません。 それでも親が更新されれば、子の関数は再び呼ばれます。

ただし、止まることもある

ここまで「親が描き直されれば子も描き直される」と書いてきました。 これは既定の動きです。ですが React には、途中で打ち切る道が 2 つ用意されています。

  1. 指示書そのものが前回と同じとき 中を見るまでもないので、その枝ごと打ち切られます
  2. 指示書は新しいが、渡している値が同じとき これは見比べないと分かりません。 だから既定では止まらず、頼んで初めて見比べてくれます

上で見た「子も描き直される」は、2 の状況です。 指示書は毎回新しく作られているので、 React は見比べる指示がないかぎり、そのまま実行します。

parent-re-renders-child.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";

function Child() {
  useTrackDemoRender();

  return (
    <div className="rounded-xl border border-slate-200 bg-slate-50 p-4 text-sm">
      <p className="font-semibold">子コンポーネント</p>
      <p>この子は props を受け取っていません。</p>
    </div>
  );
}

export function ParentRerendersChild() {
  const [count, setCount] = useState(0);

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-xl border border-slate-200 bg-slate-50 p-4">
        <p>親のカウント: {count}</p>
        <Button size="sm" onClick={() => setCount((current) => current + 1)}>
          親を更新する
        </Button>
      </div>
      <Child />
    </div>
  );
}

だから「state をどこに置くか」が効いてくる

親が動けば子も動く。この性質には、実用的な使い道があります。state を置く場所を変えるだけで、 描き直される範囲が変わります。

下は検索欄と一覧です。一覧は入力とまったく関係ありません。打ちながら、下の箱が光るかどうかを見てください。

state を外側に置いた場合うまくいかない例

打つたびに、関係のない一覧まで光る

一覧(入力とは無関係)
  • さとう
  • すずき
  • たかはし

一覧まで一緒に光っています。keyword を受け取ってすらいないのに、です。 いちばん外側が描き直されているので、中身が全部巻き込まれています。

直し方はstate を、それを使う部品の中まで下ろすことです。

// 入力欄だけを部品にして、state をその中に持たせる
function SearchBox() {
  const [keyword, setKeyword] = useState("");
  return <input value={keyword} ... />;
}
state を使う場所まで下ろした場合直した例

打っても、一覧は光らない

一覧(入力とは無関係)
  • さとう
  • すずき
  • たかはし

一覧が光らなくなりました。描き直されるのは SearchBox の中だけです。 外側は state を持っていないので、そもそも動きません。

state-up.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";

function HeavyList() {
  return (
    <RenderBox title="一覧(入力とは無関係)">
      <ul className="flex gap-2 text-sm">
        <li>さとう</li>
        <li>すずき</li>
        <li>たかはし</li>
      </ul>
    </RenderBox>
  );
}

export function StateUp() {
  useTrackDemoRender();

  // 入力欄でしか使っていない state を、いちばん外側が持っている
  const [keyword, setKeyword] = useState("");

  return (
    <div className="flex flex-col gap-3">
      <Input
        placeholder="ここに打つ"
        aria-label="ここに打つ"
        value={keyword}
        onChange={(event) => setKeyword(event.target.value)}
      />

      <Button size="sm" variant="outline" disabled>
        {keyword ? `「${keyword}」で検索` : "検索"}
      </Button>

      {/* keyword を受け取っていないのに、一緒に描き直される */}
      <HeavyList />
    </div>
  );
}

理解できたか確かめる

確認クイズ

再レンダリングとは何ですか?

確認クイズ

ページの一番上に置いた検索キーワードの state を、検索欄の中に移しました。何が変わりますか?

確認クイズ

子コンポーネントの props が同じでも再レンダリングされるのはどちらの場合?

render-counter.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";

export function RenderCounter() {
  const [count, setCount] = useState(0);
  useTrackDemoRender();

  return (
    <div className="flex flex-col gap-4">
      <p>このコンポーネントは {count} 回ボタンを押されました。</p>
      <Button size="sm" onClick={() => setCount((current) => current + 1)}>
        再レンダリングする
      </Button>
    </div>
  );
}

この章のまとめ

  • 再レンダリング=コンポーネントの関数がもう一度実行されること
  • 実行して出てくるのは画面ではなく、「こう出してくれ」という指示書(JSX)
  • React は前回の指示書と見比べて、違うところだけ実際の画面に反映する
  • だから再レンダリング=遅い、ではありません。 関数の実行と、画面の書き換えは別の段階
  • きっかけは自分の state か、親が描き直されたか。 props が変わったから、ではない
  • つまり描き直しは、上から下へ広がります
  • だからstate は、使う場所のなるべく近くに置く。 それだけで広がる範囲が狭まる

render-counter.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";

export function RenderCounter() {
  const [count, setCount] = useState(0);
  useTrackDemoRender();

  return (
    <div className="flex flex-col gap-4">
      <p>このコンポーネントは {count} 回ボタンを押されました。</p>
      <Button size="sm" onClick={() => setCount((current) => current + 1)}>
        再レンダリングする
      </Button>
    </div>
  );
}