Part 4 · 状態 (state)
26 / 64 章
useState の応用
初期値の遅延評価と、key でのリセット
前の章で、よく使う 4 つの形を見ました。 ふだん書くぶんには、あれで足ります。
この章は「そういう手があるのか」と知っておくと効く 2 つです。 どちらも知らないと、まず思いつきません。
読み流して、必要になったときに思い出せれば十分です。
1. 初期値の計算が重いとき
まず、よくある落とし穴から。
const [code] = useState(buildInitialCode());一見なんともない行です。ですがbuildInitialCode() は描き直すたびに毎回呼ばれます。
初期値として使われるのは最初の 1 回だけなのに、計算だけは毎回走ります。 括弧を書いた時点で先に実行され、その結果が渡るからです。
// ✕ 毎回呼ばれる(結果だけ捨てられる)
useState(buildInitialCode());
// ◯ 最初の 1 回だけ呼ばれる
useState(buildInitialCode);括弧を外すだけです。こう書くと「呼んだ結果」ではなく「関数そのもの」を渡すことになり、 React が必要なときだけ呼びます。
連打しても引っかからない
初期コード: CODE-9994
何度押しても引っかからない。初期値の計算は最初の 1 回だけ
Part 0 の「関数を値として扱う」が効いてくる場面です。onClick={handleClick} と onClick={handleClick()} の違いと、まったく同じ話です。
lazy-init.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
// 初期値を作るのに時間がかかる、という想定
function buildInitialCode() {
let value = 0;
for (let i = 0; i < 3_000_000; i += 1) value += i % 7;
return `CODE-${value % 10000}`;
}
export function LazyInit() {
useTrackDemoRender();
// ✕ useState(buildInitialCode()) と書くと、描き直すたびに毎回呼ばれる
// ◯ 関数を渡すと、React は最初の 1 回だけ呼ぶ
const [code] = useState(buildInitialCode);
const [count, setCount] = useState(0);
return (
<div className="flex flex-col gap-3">
<p className="font-mono text-sm">初期コード: {code}</p>
<Button size="sm" onClick={() => setCount(count + 1)}>
描き直す
</Button>
<p className="text-sm text-muted-foreground">
何度押しても引っかからない。初期値の計算は最初の 1 回だけ
</p>
</div>
);
}2. key を変えて、state ごと作り直す
こちらは、知らないとまず思いつかない手です。
感想を書く欄があり、対象を切り替えられるとします。切り替えたら、書きかけの文は消えてほしい。
素直にやるなら、切り替えのたびに setText("") を呼ぶことになります。 ですが state が増えるほど、消し忘れが出てきます。
<CommentBox key={target} target={target} />key を渡すだけです。
何か書いてから、もう片方に切り替えてみる
りんご への感想
書きかけの文が消えました。リセットのコードは 1 行も書いていません。
Part 3 の「リストと key」で、key は React が「同じものか別のものか」を見分ける目印だと説明しました。それがそのまま効いています。
key が変わると、React はそれを別のコンポーネントだと判断して作り直します。 作り直された部品の state は、当然まっさらです。
reset-by-key.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";
// 感想を書く欄。中に自分の state を持っている
function CommentBox({ target }: { target: string }) {
const [text, setText] = useState("");
return (
<div className="flex flex-col gap-2">
<p className="text-sm">{target} への感想</p>
<Input
placeholder="書いてみる"
aria-label="書いてみる"
value={text}
onChange={(event) => setText(event.target.value)}
/>
</div>
);
}
const targets = ["りんご", "みかん"];
export function ResetByKey() {
useTrackDemoRender();
const [target, setTarget] = useState(targets[0]);
return (
<div className="flex flex-col gap-3">
<div className="flex gap-2">
{targets.map((item) => (
<Button
key={item}
size="sm"
variant={target === item ? "default" : "outline"}
onClick={() => setTarget(item)}
>
{item}
</Button>
))}
</div>
{/* key を変えると、React は別物として作り直す = state が消える */}
<CommentBox key={target} target={target} />
</div>
);
}理解できたか確かめる
確認クイズ
useState(buildValue()) と useState(buildValue) の違いは?
確認クイズ
key を変えると子コンポーネントの state が消えるのはなぜ?
lazy-init.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
// 初期値を作るのに時間がかかる、という想定
function buildInitialCode() {
let value = 0;
for (let i = 0; i < 3_000_000; i += 1) value += i % 7;
return `CODE-${value % 10000}`;
}
export function LazyInit() {
useTrackDemoRender();
// ✕ useState(buildInitialCode()) と書くと、描き直すたびに毎回呼ばれる
// ◯ 関数を渡すと、React は最初の 1 回だけ呼ぶ
const [code] = useState(buildInitialCode);
const [count, setCount] = useState(0);
return (
<div className="flex flex-col gap-3">
<p className="font-mono text-sm">初期コード: {code}</p>
<Button size="sm" onClick={() => setCount(count + 1)}>
描き直す
</Button>
<p className="text-sm text-muted-foreground">
何度押しても引っかからない。初期値の計算は最初の 1 回だけ
</p>
</div>
);
}この章のまとめ
- 初期値の計算が重いなら括弧を外す(
useState(build))。最初の 1 回しか呼ばれなくなる keyを変えると、 中の state がまるごと消える。リセットのコードを書かなくてよい- どちらも困ってから思い出せば十分です
reset-by-key.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";
// 感想を書く欄。中に自分の state を持っている
function CommentBox({ target }: { target: string }) {
const [text, setText] = useState("");
return (
<div className="flex flex-col gap-2">
<p className="text-sm">{target} への感想</p>
<Input
placeholder="書いてみる"
aria-label="書いてみる"
value={text}
onChange={(event) => setText(event.target.value)}
/>
</div>
);
}
const targets = ["りんご", "みかん"];
export function ResetByKey() {
useTrackDemoRender();
const [target, setTarget] = useState(targets[0]);
return (
<div className="flex flex-col gap-3">
<div className="flex gap-2">
{targets.map((item) => (
<Button
key={item}
size="sm"
variant={target === item ? "default" : "outline"}
onClick={() => setTarget(item)}
>
{item}
</Button>
))}
</div>
{/* key を変えると、React は別物として作り直す = state が消える */}
<CommentBox key={target} target={target} />
</div>
);
}