Part 4 · 状態 (state)
state のリフトアップ
2 つの部品で同じ値を共有したくなったら
state はコンポーネントごとに持てます。持てるということは、バラバラになるということでもあります。
2 つの部品で同じ値を見せたいのに、それぞれが自分の state を持っていたら、いつまでも一致しません。
このときの定石がリフトアップです。 state を、共通の親へ引き上げます。
それぞれが持つと、そろわない
同じ部品を 2 つ置いています。 どちらのボタンを押しても、押したほうしか増えません。
上下で別々の数になる
これは不具合ではなく、当然の結果です。useState はそのコンポーネントごとに別の値を用意します。 同じコードから作られていても、置かれた 2 つは別物です。
「別々でいい」ならこれで正解です。 問題になるのは、そろっていてほしい場合です。
separate-state.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
// それぞれが自分の state を持っている
function Panel({ label }: { label: string }) {
const [count, setCount] = useState(0);
return (
<div className="flex items-center justify-between gap-3 rounded-md border p-3">
<span className="text-muted-foreground">{label}</span>
<span className="font-mono font-semibold tabular-nums">{count}</span>
<Button size="sm" onClick={() => setCount(count + 1)}>
増やす
</Button>
</div>
);
}
export function SeparateState() {
return (
<div className="flex flex-col gap-2.5">
<Panel label="上のパネル" />
<Panel label="下のパネル" />
</div>
);
}共通の親へ引き上げる
解決は単純です。両方から見える場所、つまり共通の親に state を移します。
子には、Part 2 でやった形で渡します。
- 表示するための値
- 変更を伝えるための関数
どちらを押しても、両方が変わる
子のコードから useState が消えました。 子は値を受け取って表示し、押されたら伝えるだけです。 Part 2-3 でやった形と、まったく同じになっています。
lifted-state.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
// 子は値を受け取って表示するだけ。自分では持たない
function Panel({
label,
count,
onIncrease,
}: {
label: string;
count: number;
onIncrease: () => void;
}) {
return (
<div className="flex items-center justify-between gap-3 rounded-md border p-3">
<span className="text-muted-foreground">{label}</span>
<span className="font-mono font-semibold tabular-nums">{count}</span>
<Button size="sm" onClick={onIncrease}>
増やす
</Button>
</div>
);
}
// 共通の親が値を持つ
export function LiftedState() {
const [count, setCount] = useState(0);
const increase = () => setCount((current) => current + 1);
return (
<div className="flex flex-col gap-2.5">
<Panel label="上のパネル" count={count} onIncrease={increase} />
<Panel label="下のパネル" count={count} onIncrease={increase} />
</div>
);
}どこまで上げるか
「上げれば解決する」なら、最初から一番上に置けばよさそうに思えます。 ですが、それはやりません。
上げるほど、関係のない部品まで巻き込みます。
- 渡すために、途中の部品が使いもしない props を受け渡すことになる
- 親の state が変わると、その下が広く描き直される(Part 7 以降の話)
- どこで何が変わるのか、追いかける範囲が広くなる
バケツリレーがつらくなったら
必要な高さが本当に高く、 間の部品が使いもしない props をただ受け渡すだけになることがあります。これをバケツリレーと呼びます。
// Middle は user を使わないのに、渡すためだけに受け取っている
<Page user={user}>
<Middle user={user}>
<Profile user={user} />
</Middle>
</Page>この状態が深くなってきたら、Part 9 の Context という道具が候補になります。 ただしまずはリフトアップで足ります。 2 段や 3 段で困ることはほとんどありません。
lifted-state.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
// 子は値を受け取って表示するだけ。自分では持たない
function Panel({
label,
count,
onIncrease,
}: {
label: string;
count: number;
onIncrease: () => void;
}) {
return (
<div className="flex items-center justify-between gap-3 rounded-md border p-3">
<span className="text-muted-foreground">{label}</span>
<span className="font-mono font-semibold tabular-nums">{count}</span>
<Button size="sm" onClick={onIncrease}>
増やす
</Button>
</div>
);
}
// 共通の親が値を持つ
export function LiftedState() {
const [count, setCount] = useState(0);
const increase = () => setCount((current) => current + 1);
return (
<div className="flex flex-col gap-2.5">
<Panel label="上のパネル" count={count} onIncrease={increase} />
<Panel label="下のパネル" count={count} onIncrease={increase} />
</div>
);
}理解できたか確かめる
確認クイズ
同じコンポーネントを 2 つ置くと、state はどうなる?
確認クイズ
state の置き場所として適切なのは?
lifted-state.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
// 子は値を受け取って表示するだけ。自分では持たない
function Panel({
label,
count,
onIncrease,
}: {
label: string;
count: number;
onIncrease: () => void;
}) {
return (
<div className="flex items-center justify-between gap-3 rounded-md border p-3">
<span className="text-muted-foreground">{label}</span>
<span className="font-mono font-semibold tabular-nums">{count}</span>
<Button size="sm" onClick={onIncrease}>
増やす
</Button>
</div>
);
}
// 共通の親が値を持つ
export function LiftedState() {
const [count, setCount] = useState(0);
const increase = () => setCount((current) => current + 1);
return (
<div className="flex flex-col gap-2.5">
<Panel label="上のパネル" count={count} onIncrease={increase} />
<Panel label="下のパネル" count={count} onIncrease={increase} />
</div>
);
}この章のまとめ
useStateはコンポーネントごとに別の値を持つ- そろえたいときは、共通の親へ state を上げる
- 子には、値と変更を伝える関数を props で渡す
- 置き場所は必要な部品すべてを含む、いちばん下
- バケツリレーが深くなったら Context(Part 9)。 ただし多くの場合、リフトアップと合成で足りる
lifted-state.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
// 子は値を受け取って表示するだけ。自分では持たない
function Panel({
label,
count,
onIncrease,
}: {
label: string;
count: number;
onIncrease: () => void;
}) {
return (
<div className="flex items-center justify-between gap-3 rounded-md border p-3">
<span className="text-muted-foreground">{label}</span>
<span className="font-mono font-semibold tabular-nums">{count}</span>
<Button size="sm" onClick={onIncrease}>
増やす
</Button>
</div>
);
}
// 共通の親が値を持つ
export function LiftedState() {
const [count, setCount] = useState(0);
const increase = () => setCount((current) => current + 1);
return (
<div className="flex flex-col gap-2.5">
<Panel label="上のパネル" count={count} onIncrease={increase} />
<Panel label="下のパネル" count={count} onIncrease={increase} />
</div>
);
}