【第20回】データを変えれば画面が追いつく形にする|useState / useEffect(嫁子が教えるTS講座)
今回のゴール
useStateで「変わる値」を持てる- 配列を直接書き換えてはいけない理由が分かる
useEffectが要る場面と、要らない場面を見分けられる
前回(コンポーネントとProps)までは固定のデータを表示していました。今回から動きます。全体の流れは講座の目次へ。
嫁子: 今日で、第17回で話した「データを変えれば画面が追いつく」が実物になるよ。
render()を呼ぶ場面が、この回から一度も出てこないの。そこを意識しながら読んでみて。
① state — 変わる値を持つ
まず「使う側」の宣言から。ファイルの先頭にこれを書きます。
"use client";
Next.jsでは、ページは既定でサーバー側で作られます。ボタンを押すと変わるような画面は、ブラウザ側で動かす宣言が必要です。理由は第5部で扱うので、今は「操作を伴う画面には付ける」で構いません。
そのうえで useState を使います。
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count} 回
</button>
);
}
読み方はこうです。
count— 今の値setCount— 値を変える関数useState(0)— 最初の値は0
ボタンを押すと setCount が呼ばれ、その部品だけが描き直されます。render() に当たるものは書いていません。
普通の変数ではダメなのか、という疑問が出ます。
let count = 0; // ✕ これでは画面が変わらない
普通の変数を書き換えても、Reactは「変わった」と気づけません。setCount を通すことで初めて、描き直しが起きます。
嫁子: 「画面に出る値」は state にする、が判断の目安だよ。
逆に、計算の途中で使うだけの値は普通の変数でいいの。全部stateにすると、かえって読みにくくなるからね。
② 配列を state にする
第10回の買い物メモを、Reactで作り直してみます。
"use client";
import { useState } from "react";
type Item = {
id: number;
name: string;
done: boolean;
};
export default function Page() {
const [items, setItems] = useState<Item[]>([]);
function addItem(name: string) {
setItems([...items, { id: Date.now(), name: name, done: false }]);
}
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
useState<Item[]>([]) の山カッコは、第14回でやったジェネリクスです。空の配列から始めるので、中身の型を書く必要があります。
③ 直接書き換えてはいけない
ここが今日いちばん重要です。
items.push({ id: 1, name: "牛乳", done: false }); // ✕ 画面が変わらない
setItems([...items, { id: 1, name: "牛乳", done: false }]); // ○
push は元の配列を変える書き方です。Reactは「配列そのものが入れ替わったか」で変化を判断するので、中身だけ変わっても気づけません。
...items は「今の中身を全部並べる」という記法(スプレッド構文)です。それに新しい要素を足した別の配列を作って渡しています。
削除と更新も同じ考え方です。
// 削除:条件に合うものだけ残した新しい配列
setItems(items.filter((item) => item.id !== id));
// 更新:該当するものだけ差し替えた新しい配列
setItems(items.map((item) =>
item.id === id ? { ...item, done: !item.done } : item
));
第5回でやった filter と map が、ここで主役になります。どちらも元の配列を変えずに新しい配列を返す関数だからです。
嫁子: 「元を変えずに、新しく作る」。これが第4部を通しての合言葉だと思って。
pushsplicesortあたりは元を書き換えるから、stateには使わないの。第10回では普通にspliceを使ってたでしょ。あそこと考え方が変わるところだよ。
④ useEffect — 外の世界と同期する
もう一つよく見るのが useEffect です。
useEffect(() => {
// 画面が表示されたあとに実行される
}, []);
最後の [] は依存配列といって、いつ実行するかを指定します。
| 書き方 | 実行されるタイミング |
|---|---|
[] |
最初の1回だけ |
[id] |
id が変わるたび |
| 省略 | 描き直されるたび(ほぼ使わない) |
使うのは、Reactの外側とやりとりするときです。サーバーからデータを取ってくる、ブラウザの保存領域を読む、タイマーを仕掛ける、といった場面が該当します。
useEffect(() => {
const saved = localStorage.getItem("items");
if (saved !== null) {
setItems(JSON.parse(saved));
}
}, []);
⑤ useEffect は、疑ってから使う
ここがAIと組むときの注意点です。AIは useEffect を必要以上に使うことがあります。
典型が「他のstateから計算できる値を、わざわざstateにする」パターンです。
// ✕ 要らない useEffect
const [items, setItems] = useState<Item[]>([]);
const [remain, setRemain] = useState(0);
useEffect(() => {
setRemain(items.filter((item) => !item.done).length);
}, [items]);
残り件数は items から計算できるので、そのまま計算すれば済みます。
// ○ ただの計算でいい
const remain = items.filter((item) => !item.done).length;
余計なstateを持つと、片方だけ更新されてズレる事故が起きます。判断の目安は「他の値から計算できるか」です。計算できるなら、stateにしないでください。
嫁子: これ、AIのコードを読むときの検査項目にしていいと思う。
useEffectを見つけたら「これ、外の世界と何かやりとりしてる?」と聞いてみて。していないなら、たいてい要らない。「この
useEffectは本当に必要ですか。計算で置き換えられませんか」って投げると、素直に直してくれるよ。
つまずきポイント
フックは関数の一番上で呼びます。if の中や for の中では呼べません。呼ぶ順番が変わると、Reactが値を取り違えるからです。
setCount(count + 1) を連続で2回書いても、2つ増えません。どちらも同じ count を見ているためです。前の値を元にしたいときは、関数を渡します。
setCount((prev) => prev + 1);
"use client" を忘れると、useState を使った時点でエラーになります。エラー文にそう書いてあるので、素直に足してください。
今回のまとめ
const [値, 設定する関数] = useState(初期値)で、変わる値を持つ- 画面に出る値は state に、計算で出せる値は普通の変数に
- 配列や オブジェクトは直接書き換えず、新しく作って渡す(
pushではなく[...items, 新]) useEffectは外の世界とやりとりするときに使う- AIの
useEffectは疑う。他の値から計算できるなら要らない
次にやること
次回はフォームです。入力欄から値を受け取って、この items に足せるようにします。第4回の「フォームの数字は文字列で届く」が、そこで戻ってきます。
今回の宿題:カウンターを作って、setCount(count + 1) を2回続けて書いたらどうなるか試してください。1しか増えません。理由が腑に落ちると、stateの仕組みが分かったことになります。
嫁子: 「2回書いたのに1しか増えない」って、バグに見えるでしょ。でも仕様なの。
押した時点の
countを、2回とも見てるからだね。ここは理屈で覚えるより、一度自分で見たほうが早いよ。