Part 9 · state を共有する
49 / 64 章
Context と再レンダリング
1 つの Context に詰め込むと何が起きるか
Context は便利なので、つい「アプリの状態を全部ここに入れよう」と考えたくなります。
それをやると、関係のない部品まで一斉に描き直されます。 しかも気づきにくい形で。
ひとつにまとめた場合
count と name を、 ひとつの Context に入れてあります。 そして下には、それぞれ片方しか使っていない部品が並んでいます。
「count を増やす」を押してください。
count を押すと、name の部品まで光る
name しか使っていない部品まで光ります。name の値はまったく変わっていないのに、です。
large-context.tsx
"use client";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { createContext, type ReactNode, use, useState } from "react";
type Store = {
count: number;
setCount: (update: (current: number) => number) => void;
name: string;
setName: (update: (current: string) => string) => void;
};
// count も name も、ひとつの置き場所にまとめている
const LargeContext = createContext<Store>({} as Store);
function LargeProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
const [name, setName] = useState("さとう");
return (
<LargeContext value={{ count, setCount, name, setName }}>
{children}
</LargeContext>
);
}
// count しか使っていない
function CountDisplay() {
const { count } = use(LargeContext);
return <RenderBox title="count だけを使う部品">count: {count}</RenderBox>;
}
// name しか使っていない
function NameDisplay() {
const { name } = use(LargeContext);
return <RenderBox title="name だけを使う部品">name: {name}</RenderBox>;
}
function Controls() {
// 更新するだけなので、値そのものは受け取らない
const { setCount, setName } = use(LargeContext);
return (
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={() => setCount((current) => current + 1)}>
count を増やす
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setName((current) => (current === "さとう" ? "すずき" : "さとう"))}
>
name を変える
</Button>
</div>
);
}
export function LargeContextDemo() {
return (
<LargeProvider>
<div className="flex flex-col gap-4">
<Controls />
<CountDisplay />
<NameDisplay />
</div>
</LargeProvider>
);
}なぜ関係ない部品まで描き直されるのか
Part 7 で「再レンダリングの理由は、いまのところ 2 つ」と書きました。これが 3 つめです。自分の state でも、親の描き直しでもなく、見に行っている Context が変わったとき。
理由は 2 つ重なっています。
1. Context は「項目ごと」に見てくれない
use(LargeContext) は、その Context 全体を購読します。 「name だけを見ている」と書いたつもりでも、React はそう解釈しません。
// name だけ取り出しているように見えるが…
const { name } = use(LargeContext);
// React から見ると「LargeContext を購読している」だけReact が見ているのはvalue に渡したものが、前と別のものになったかどうかだけです。 別のものになっていれば、購読している部品はすべて描き直されます。どの項目を使っているかは、見ていません。
2. value のオブジェクトが毎回新しく作られる
<LargeContext value={{ count, setCount, name, setName }}>この { ... } は、 描き直されるたびに新しいオブジェクトになります。 Part 8 で何度も見た形です。
count が変わって Provider が描き直されると、 中身が同じでも value は別のものになります。 購読している側から見れば「変わった」ことになります。
large-context.tsx
"use client";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { createContext, type ReactNode, use, useState } from "react";
type Store = {
count: number;
setCount: (update: (current: number) => number) => void;
name: string;
setName: (update: (current: string) => string) => void;
};
// count も name も、ひとつの置き場所にまとめている
const LargeContext = createContext<Store>({} as Store);
function LargeProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
const [name, setName] = useState("さとう");
return (
<LargeContext value={{ count, setCount, name, setName }}>
{children}
</LargeContext>
);
}
// count しか使っていない
function CountDisplay() {
const { count } = use(LargeContext);
return <RenderBox title="count だけを使う部品">count: {count}</RenderBox>;
}
// name しか使っていない
function NameDisplay() {
const { name } = use(LargeContext);
return <RenderBox title="name だけを使う部品">name: {name}</RenderBox>;
}
function Controls() {
// 更新するだけなので、値そのものは受け取らない
const { setCount, setName } = use(LargeContext);
return (
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={() => setCount((current) => current + 1)}>
count を増やす
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setName((current) => (current === "さとう" ? "すずき" : "さとう"))}
>
name を変える
</Button>
</div>
);
}
export function LargeContextDemo() {
return (
<LargeProvider>
<div className="flex flex-col gap-4">
<Controls />
<CountDisplay />
<NameDisplay />
</div>
</LargeProvider>
);
}関心ごとに分ける
解決は単純です。置き場所を分けます。
// ひとつにまとめる代わりに
const CountContext = createContext(...);
const NameContext = createContext(...);count を押しても、name の部品は光らない
片方だけが光るようになりました。name の部品は NameContext しか購読していないので、count の変化は届きません。
split-context.tsx
"use client";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { createContext, type ReactNode, use, useMemo, useState } from "react";
// 置き場所を関心ごとに分ける
const CountContext = createContext<{
count: number;
setCount: (update: (current: number) => number) => void;
}>({ count: 0, setCount: () => {} });
const NameContext = createContext<{
name: string;
setName: (update: (current: string) => string) => void;
}>({ name: "", setName: () => {} });
function CountProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
// count が変わったときだけ、新しい value を作る
const value = useMemo(() => ({ count, setCount }), [count]);
return <CountContext value={value}>{children}</CountContext>;
}
function NameProvider({ children }: { children: ReactNode }) {
const [name, setName] = useState("さとう");
const value = useMemo(() => ({ name, setName }), [name]);
return <NameContext value={value}>{children}</NameContext>;
}
// count の置き場所だけを見る
function CountDisplay() {
const { count } = use(CountContext);
return (
<RenderBox title="count だけを使う部品" tone="highlight">
count: {count}
</RenderBox>
);
}
// name の置き場所だけを見る
function NameDisplay() {
const { name } = use(NameContext);
return (
<RenderBox title="name だけを使う部品" tone="highlight">
name: {name}
</RenderBox>
);
}
function Controls() {
// 更新するだけなので、値そのものは受け取らない
const { setCount } = use(CountContext);
const { setName } = use(NameContext);
return (
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={() => setCount((current) => current + 1)}>
count を増やす
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setName((current) => (current === "さとう" ? "すずき" : "さとう"))}
>
name を変える
</Button>
</div>
);
}
export function SplitContextDemo() {
return (
<CountProvider>
<NameProvider>
<div className="flex flex-col gap-4">
<Controls />
<CountDisplay />
<NameDisplay />
</div>
</NameProvider>
</CountProvider>
);
}分けただけでは、まだ半分
ここまでで原因 1(項目単位で購読できない)は消えました。 ですが原因 2 はまだ残っています。value に { count, setCount } と直接書くと、 Provider が描き直されるたびに新しいオブジェクトになります。
これを止める道具は、Part 8 でもう手に入れています。useMemo です。
// ✕ 描き直されるたびに、新しいオブジェクト
return <CountContext value={{ count, setCount }}>{children}</CountContext>;
// ○ count が変わったときだけ、新しいオブジェクト
const value = useMemo(() => ({ count, setCount }), [count]);
return <CountContext value={value}>{children}</CountContext>;いまのデモでは、count が変われば どのみち購読側は描き直されるので、この 1 行で見た目が変わるわけではありません。 効いてくるのは、Provider が別の理由で描き直されたときです。
たとえば Provider に別の state が増えたとき。value を包んでいなければ、count は 1 ミリも変わっていないのにcount を使う部品が全部描き直されます。 包んであれば、何も起きません。
split-context.tsx
"use client";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { createContext, type ReactNode, use, useMemo, useState } from "react";
// 置き場所を関心ごとに分ける
const CountContext = createContext<{
count: number;
setCount: (update: (current: number) => number) => void;
}>({ count: 0, setCount: () => {} });
const NameContext = createContext<{
name: string;
setName: (update: (current: string) => string) => void;
}>({ name: "", setName: () => {} });
function CountProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
// count が変わったときだけ、新しい value を作る
const value = useMemo(() => ({ count, setCount }), [count]);
return <CountContext value={value}>{children}</CountContext>;
}
function NameProvider({ children }: { children: ReactNode }) {
const [name, setName] = useState("さとう");
const value = useMemo(() => ({ name, setName }), [name]);
return <NameContext value={value}>{children}</NameContext>;
}
// count の置き場所だけを見る
function CountDisplay() {
const { count } = use(CountContext);
return (
<RenderBox title="count だけを使う部品" tone="highlight">
count: {count}
</RenderBox>
);
}
// name の置き場所だけを見る
function NameDisplay() {
const { name } = use(NameContext);
return (
<RenderBox title="name だけを使う部品" tone="highlight">
name: {name}
</RenderBox>
);
}
function Controls() {
// 更新するだけなので、値そのものは受け取らない
const { setCount } = use(CountContext);
const { setName } = use(NameContext);
return (
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={() => setCount((current) => current + 1)}>
count を増やす
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setName((current) => (current === "さとう" ? "すずき" : "さとう"))}
>
name を変える
</Button>
</div>
);
}
export function SplitContextDemo() {
return (
<CountProvider>
<NameProvider>
<div className="flex flex-col gap-4">
<Controls />
<CountDisplay />
<NameDisplay />
</div>
</NameProvider>
</CountProvider>
);
}Provider が children を受け取っている理由
デモの Provider は、こういう形をしています。
function CountProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
const value = useMemo(() => ({ count, setCount }), [count]);
return <CountContext value={value}>{children}</CountContext>;
}これは Part 8 の「children で切り離す」と同じ形です。 そして、この形でなければデモが成立しません。
もし Provider の中に子を直接書いていたら、count が変わって Provider が描き直されるたびに、Context とは関係なく子も全部描き直されます。 それでは「Context を分けた効果」が見えません。
children として外から渡すことで、Context による描き直しだけを取り出して観察できています。
split-context.tsx
"use client";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { createContext, type ReactNode, use, useMemo, useState } from "react";
// 置き場所を関心ごとに分ける
const CountContext = createContext<{
count: number;
setCount: (update: (current: number) => number) => void;
}>({ count: 0, setCount: () => {} });
const NameContext = createContext<{
name: string;
setName: (update: (current: string) => string) => void;
}>({ name: "", setName: () => {} });
function CountProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
// count が変わったときだけ、新しい value を作る
const value = useMemo(() => ({ count, setCount }), [count]);
return <CountContext value={value}>{children}</CountContext>;
}
function NameProvider({ children }: { children: ReactNode }) {
const [name, setName] = useState("さとう");
const value = useMemo(() => ({ name, setName }), [name]);
return <NameContext value={value}>{children}</NameContext>;
}
// count の置き場所だけを見る
function CountDisplay() {
const { count } = use(CountContext);
return (
<RenderBox title="count だけを使う部品" tone="highlight">
count: {count}
</RenderBox>
);
}
// name の置き場所だけを見る
function NameDisplay() {
const { name } = use(NameContext);
return (
<RenderBox title="name だけを使う部品" tone="highlight">
name: {name}
</RenderBox>
);
}
function Controls() {
// 更新するだけなので、値そのものは受け取らない
const { setCount } = use(CountContext);
const { setName } = use(NameContext);
return (
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={() => setCount((current) => current + 1)}>
count を増やす
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setName((current) => (current === "さとう" ? "すずき" : "さとう"))}
>
name を変える
</Button>
</div>
);
}
export function SplitContextDemo() {
return (
<CountProvider>
<NameProvider>
<div className="flex flex-col gap-4">
<Controls />
<CountDisplay />
<NameDisplay />
</div>
</NameProvider>
</CountProvider>
);
}どう分けるか
分ける基準は「一緒に変わるかどうか」です。
- 一緒に変わるものは同じ Context でよい (ユーザー情報の名前とメールアドレスなど)
- 別々に変わるものは分ける (ユーザー情報とテーマ設定など)
- 更新が頻繁なものは必ず分ける。 巻き込む回数がそのまま増える
値と更新関数を分ける手もある
const CountContext = createContext(0); // 値
const SetCountContext = createContext(() => {}); // 更新関数「更新はするが表示はしない」部品は珍しくありません。 そういう部品は SetCountContext だけを購読すれば、値が変わっても描き直されなくなります。
split-context.tsx
"use client";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { createContext, type ReactNode, use, useMemo, useState } from "react";
// 置き場所を関心ごとに分ける
const CountContext = createContext<{
count: number;
setCount: (update: (current: number) => number) => void;
}>({ count: 0, setCount: () => {} });
const NameContext = createContext<{
name: string;
setName: (update: (current: string) => string) => void;
}>({ name: "", setName: () => {} });
function CountProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
// count が変わったときだけ、新しい value を作る
const value = useMemo(() => ({ count, setCount }), [count]);
return <CountContext value={value}>{children}</CountContext>;
}
function NameProvider({ children }: { children: ReactNode }) {
const [name, setName] = useState("さとう");
const value = useMemo(() => ({ name, setName }), [name]);
return <NameContext value={value}>{children}</NameContext>;
}
// count の置き場所だけを見る
function CountDisplay() {
const { count } = use(CountContext);
return (
<RenderBox title="count だけを使う部品" tone="highlight">
count: {count}
</RenderBox>
);
}
// name の置き場所だけを見る
function NameDisplay() {
const { name } = use(NameContext);
return (
<RenderBox title="name だけを使う部品" tone="highlight">
name: {name}
</RenderBox>
);
}
function Controls() {
// 更新するだけなので、値そのものは受け取らない
const { setCount } = use(CountContext);
const { setName } = use(NameContext);
return (
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={() => setCount((current) => current + 1)}>
count を増やす
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setName((current) => (current === "さとう" ? "すずき" : "さとう"))}
>
name を変える
</Button>
</div>
);
}
export function SplitContextDemo() {
return (
<CountProvider>
<NameProvider>
<div className="flex flex-col gap-4">
<Controls />
<CountDisplay />
<NameDisplay />
</div>
</NameProvider>
</CountProvider>
);
}理解できたか確かめる
確認クイズ
const { name } = use(LargeContext) と書けば、name が変わったときだけ描き直される?
確認クイズ
Provider が children を受け取る形にする理由は?
large-context.tsx
"use client";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { createContext, type ReactNode, use, useState } from "react";
type Store = {
count: number;
setCount: (update: (current: number) => number) => void;
name: string;
setName: (update: (current: string) => string) => void;
};
// count も name も、ひとつの置き場所にまとめている
const LargeContext = createContext<Store>({} as Store);
function LargeProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
const [name, setName] = useState("さとう");
return (
<LargeContext value={{ count, setCount, name, setName }}>
{children}
</LargeContext>
);
}
// count しか使っていない
function CountDisplay() {
const { count } = use(LargeContext);
return <RenderBox title="count だけを使う部品">count: {count}</RenderBox>;
}
// name しか使っていない
function NameDisplay() {
const { name } = use(LargeContext);
return <RenderBox title="name だけを使う部品">name: {name}</RenderBox>;
}
function Controls() {
// 更新するだけなので、値そのものは受け取らない
const { setCount, setName } = use(LargeContext);
return (
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={() => setCount((current) => current + 1)}>
count を増やす
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setName((current) => (current === "さとう" ? "すずき" : "さとう"))}
>
name を変える
</Button>
</div>
);
}
export function LargeContextDemo() {
return (
<LargeProvider>
<div className="flex flex-col gap-4">
<Controls />
<CountDisplay />
<NameDisplay />
</div>
</LargeProvider>
);
}この章のまとめ
- Context は項目ごとではなく全体を購読する。 使っていない項目の変化でも描き直される
- value のオブジェクトは毎回新しく作られるので、Provider が描き直されれば必ず「変わった」ことになる
- 対処は一緒に変わるものだけ、同じ箱に入れること。基準は「一緒に変わるかどうか」
- Provider はchildren を受け取る薄い部品にする。 中に直接書くと分割の効果が半減する
- ただし、やりすぎない。実際に遅くなってから分ける
split-context.tsx
"use client";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { createContext, type ReactNode, use, useMemo, useState } from "react";
// 置き場所を関心ごとに分ける
const CountContext = createContext<{
count: number;
setCount: (update: (current: number) => number) => void;
}>({ count: 0, setCount: () => {} });
const NameContext = createContext<{
name: string;
setName: (update: (current: string) => string) => void;
}>({ name: "", setName: () => {} });
function CountProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
// count が変わったときだけ、新しい value を作る
const value = useMemo(() => ({ count, setCount }), [count]);
return <CountContext value={value}>{children}</CountContext>;
}
function NameProvider({ children }: { children: ReactNode }) {
const [name, setName] = useState("さとう");
const value = useMemo(() => ({ name, setName }), [name]);
return <NameContext value={value}>{children}</NameContext>;
}
// count の置き場所だけを見る
function CountDisplay() {
const { count } = use(CountContext);
return (
<RenderBox title="count だけを使う部品" tone="highlight">
count: {count}
</RenderBox>
);
}
// name の置き場所だけを見る
function NameDisplay() {
const { name } = use(NameContext);
return (
<RenderBox title="name だけを使う部品" tone="highlight">
name: {name}
</RenderBox>
);
}
function Controls() {
// 更新するだけなので、値そのものは受け取らない
const { setCount } = use(CountContext);
const { setName } = use(NameContext);
return (
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={() => setCount((current) => current + 1)}>
count を増やす
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setName((current) => (current === "さとう" ? "すずき" : "さとう"))}
>
name を変える
</Button>
</div>
);
}
export function SplitContextDemo() {
return (
<CountProvider>
<NameProvider>
<div className="flex flex-col gap-4">
<Controls />
<CountDisplay />
<NameDisplay />
</div>
</NameProvider>
</CountProvider>
);
}