【第20回】データを変えれば画面が追いつく形にする|useState / useEffect(嫁子が教えるTS講座)

今回のゴール

前回(コンポーネントと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>
  );
}

読み方はこうです。

ボタンを押すと 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部を通しての合言葉だと思って。

push splice sort あたりは元を書き換えるから、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 を使った時点でエラーになります。エラー文にそう書いてあるので、素直に足してください。


今回のまとめ


次にやること

次回はフォームです。入力欄から値を受け取って、この items に足せるようにします。第4回の「フォームの数字は文字列で届く」が、そこで戻ってきます。

今回の宿題:カウンターを作って、setCount(count + 1) を2回続けて書いたらどうなるか試してください。1しか増えません。理由が腑に落ちると、stateの仕組みが分かったことになります。

嫁子: 「2回書いたのに1しか増えない」って、バグに見えるでしょ。でも仕様なの。

押した時点の count を、2回とも見てるからだね。ここは理屈で覚えるより、一度自分で見たほうが早いよ。