本文へスキップ
useState の応用

Part 4 · 状態 (state)

26 / 64

useState の応用

初期値の遅延評価と、key でのリセット

前の章で、よく使う 4 つの形を見ました。 ふだん書くぶんには、あれで足ります。

この章は「そういう手があるのか」と知っておくと効く 2 つです。 どちらも知らないと、まず思いつきません

読み流して、必要になったときに思い出せれば十分です。

1. 初期値の計算が重いとき

まず、よくある落とし穴から。

const [code] = useState(buildInitialCode());

一見なんともない行です。ですがbuildInitialCode() は描き直すたびに毎回呼ばれます

初期値として使われるのは最初の 1 回だけなのに、計算だけは毎回走ります。 括弧を書いた時点で先に実行され、その結果が渡るからです。

// ✕ 毎回呼ばれる(結果だけ捨てられる)
useState(buildInitialCode());

// ◯ 最初の 1 回だけ呼ばれる
useState(buildInitialCode);

括弧を外すだけです。こう書くと「呼んだ結果」ではなく「関数そのもの」を渡すことになり、 React が必要なときだけ呼びます。

初期値の計算を 1 回で済ませる直した例

連打しても引っかからない

初期コード: 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 を渡すだけです。

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>
  );
}