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 つ用意されています。
- 指示書そのものが前回と同じとき … 中を見るまでもないので、その枝ごと打ち切られます
- 指示書は新しいが、渡している値が同じとき … これは見比べないと分かりません。 だから既定では止まらず、頼んで初めて見比べてくれます
上で見た「子も描き直される」は、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 を置く場所を変えるだけで、 描き直される範囲が変わります。
下は検索欄と一覧です。一覧は入力とまったく関係ありません。打ちながら、下の箱が光るかどうかを見てください。
打つたびに、関係のない一覧まで光る
- さとう
- すずき
- たかはし
一覧まで一緒に光っています。keyword を受け取ってすらいないのに、です。 いちばん外側が描き直されているので、中身が全部巻き込まれています。
直し方はstate を、それを使う部品の中まで下ろすことです。
// 入力欄だけを部品にして、state をその中に持たせる
function SearchBox() {
const [keyword, setKeyword] = useState("");
return <input value={keyword} ... />;
}打っても、一覧は光らない
- さとう
- すずき
- たかはし
一覧が光らなくなりました。描き直されるのは 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>
);
}