本文へスキップ
状態の置き場所を選ぶ

Part 9 · state を共有する

55 / 64

状態の置き場所を選ぶ

4 つの選択肢を、同じ形で並べて見比べる

この Part で、状態の置き場所を 4 つ見てきました。 メモリ、URL、ブラウザの保存領域、サーバー。

気づいたかもしれませんが、書き方はどれもほとんど同じでした。 それは偶然ではありません。

この章で、選び方をはっきりさせます。

形は同じ、置き場所が違うだけ

const [value, setValue] = useState("");
const [value, setValue] = useQueryState("key", { defaultValue: "" });
const [value, setValue] = useLocalStorageState("key", { defaultValue: "" });

並べてみると、まったく同じ形をしています。受け取るのは「いまの値」と「変える関数」。使い方も同じです。

これは、それぞれのライブラリがわざと useState に似せて作られているからです。 新しい置き場所を覚えるたびに、新しい書き方を覚え直す必要はありません。

3 つの置き場所を並べる

全部に入力してから、再読み込みしてみる

読み込み中…

再読み込みすると、はっきり分かれます。

  • メモリ … 消えます
  • URL … 残ります(アドレス欄にも出ています)
  • ブラウザの保存領域 … 残ります

コードの形は同じでも、置き場所を変えるだけで振る舞いが変わります。 だから「どこに置くか」は、あとから効いてくる選択になります。

compare.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useQueryState } from "nuqs";
import { useState } from "react";
import useLocalStorageState from "use-local-storage-state";

export function Compare() {
  useTrackDemoRender();

  // 3 つとも同じ形。違うのは置き場所だけ
  const [inMemory, setInMemory] = useState("");
  const [inUrl, setInUrl] = useQueryState("memo", { defaultValue: "" });
  const [inStorage, setInStorage] = useLocalStorageState(
    "react-lesson-compare",
    { defaultValue: "" },
  );

  return (
    <div className="flex flex-col gap-4">
      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">useState(メモリ)</span>
        <Input
          value={inMemory}
          onChange={(event) => setInMemory(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">useQueryState(URL)</span>
        <Input
          value={inUrl}
          onChange={(event) => setInUrl(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">
          useLocalStorageState(ブラウザ)
        </span>
        <Input
          value={inStorage}
          onChange={(event) => setInStorage(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <Button
        size="sm"
        variant="outline"
        onClick={() => window.location.reload()}
      >
        3 つとも入力してから、ここを押して再読み込み
      </Button>
    </div>
  );
}

4 つの比較

置き場所リロード共有向いているもの
useState消えるできない入力途中、開閉の状態
URL残るできる検索条件、並び順、タブ
ブラウザ保存残るできない表示設定、下書き
サーバー残るできる本物のデータ全般

下に行くほど「残る」度合いが強くなります。そして残るほど、扱いに気をつける必要が出てきます。

compare.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useQueryState } from "nuqs";
import { useState } from "react";
import useLocalStorageState from "use-local-storage-state";

export function Compare() {
  useTrackDemoRender();

  // 3 つとも同じ形。違うのは置き場所だけ
  const [inMemory, setInMemory] = useState("");
  const [inUrl, setInUrl] = useQueryState("memo", { defaultValue: "" });
  const [inStorage, setInStorage] = useLocalStorageState(
    "react-lesson-compare",
    { defaultValue: "" },
  );

  return (
    <div className="flex flex-col gap-4">
      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">useState(メモリ)</span>
        <Input
          value={inMemory}
          onChange={(event) => setInMemory(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">useQueryState(URL)</span>
        <Input
          value={inUrl}
          onChange={(event) => setInUrl(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">
          useLocalStorageState(ブラウザ)
        </span>
        <Input
          value={inStorage}
          onChange={(event) => setInStorage(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <Button
        size="sm"
        variant="outline"
        onClick={() => window.location.reload()}
      >
        3 つとも入力してから、ここを押して再読み込み
      </Button>
    </div>
  );
}

選び方

上から順に問いかけて、最初に当てはまったところに置きます。

迷ったら useState で始めてかまいません。形が同じなので、あとから移すのが簡単だからです。

// あとで URL に移したくなったら、1 行だけ書き換える
- const [keyword, setKeyword] = useState("");
+ const [keyword, setKeyword] = useQueryState("keyword", { defaultValue: "" });

使っている場所には手を触れません。 これが「形を揃えてある」ことの実利です。

compare.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useQueryState } from "nuqs";
import { useState } from "react";
import useLocalStorageState from "use-local-storage-state";

export function Compare() {
  useTrackDemoRender();

  // 3 つとも同じ形。違うのは置き場所だけ
  const [inMemory, setInMemory] = useState("");
  const [inUrl, setInUrl] = useQueryState("memo", { defaultValue: "" });
  const [inStorage, setInStorage] = useLocalStorageState(
    "react-lesson-compare",
    { defaultValue: "" },
  );

  return (
    <div className="flex flex-col gap-4">
      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">useState(メモリ)</span>
        <Input
          value={inMemory}
          onChange={(event) => setInMemory(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">useQueryState(URL)</span>
        <Input
          value={inUrl}
          onChange={(event) => setInUrl(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">
          useLocalStorageState(ブラウザ)
        </span>
        <Input
          value={inStorage}
          onChange={(event) => setInStorage(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <Button
        size="sm"
        variant="outline"
        onClick={() => window.location.reload()}
      >
        3 つとも入力してから、ここを押して再読み込み
      </Button>
    </div>
  );
}

Context はどこに入るのか

この表に Context がないことに気づいたかもしれません。 それには理由があります。

だから「Context に置くか、useState に置くか」という問いは、 そもそも成り立ちません。正しくは「useState で持ったものを、Context で配るかどうか」です。

compare.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useQueryState } from "nuqs";
import { useState } from "react";
import useLocalStorageState from "use-local-storage-state";

export function Compare() {
  useTrackDemoRender();

  // 3 つとも同じ形。違うのは置き場所だけ
  const [inMemory, setInMemory] = useState("");
  const [inUrl, setInUrl] = useQueryState("memo", { defaultValue: "" });
  const [inStorage, setInStorage] = useLocalStorageState(
    "react-lesson-compare",
    { defaultValue: "" },
  );

  return (
    <div className="flex flex-col gap-4">
      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">useState(メモリ)</span>
        <Input
          value={inMemory}
          onChange={(event) => setInMemory(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">useQueryState(URL)</span>
        <Input
          value={inUrl}
          onChange={(event) => setInUrl(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">
          useLocalStorageState(ブラウザ)
        </span>
        <Input
          value={inStorage}
          onChange={(event) => setInStorage(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <Button
        size="sm"
        variant="outline"
        onClick={() => window.location.reload()}
      >
        3 つとも入力してから、ここを押して再読み込み
      </Button>
    </div>
  );
}

理解できたか確かめる

確認クイズ

「文字を大きく表示する」という表示設定は、どこに置くのがよい?

確認クイズ

「Context に置くか、useState に置くか」という問いは?

確認クイズ

useState / URL / ブラウザ保存の 3 つが、同じ書き方に似せてあるのはなぜ?

compare.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useQueryState } from "nuqs";
import { useState } from "react";
import useLocalStorageState from "use-local-storage-state";

export function Compare() {
  useTrackDemoRender();

  // 3 つとも同じ形。違うのは置き場所だけ
  const [inMemory, setInMemory] = useState("");
  const [inUrl, setInUrl] = useQueryState("memo", { defaultValue: "" });
  const [inStorage, setInStorage] = useLocalStorageState(
    "react-lesson-compare",
    { defaultValue: "" },
  );

  return (
    <div className="flex flex-col gap-4">
      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">useState(メモリ)</span>
        <Input
          value={inMemory}
          onChange={(event) => setInMemory(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">useQueryState(URL)</span>
        <Input
          value={inUrl}
          onChange={(event) => setInUrl(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">
          useLocalStorageState(ブラウザ)
        </span>
        <Input
          value={inStorage}
          onChange={(event) => setInStorage(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <Button
        size="sm"
        variant="outline"
        onClick={() => window.location.reload()}
      >
        3 つとも入力してから、ここを押して再読み込み
      </Button>
    </div>
  );
}

この章のまとめ

  • 置き場所は 4 つ。うちメモリ・URL・ブラウザ保存の 3 つは 同じ書き方(サーバーだけは形が違う)
  • 選ぶ順番は、本物のデータか → 共有したいか → 覚えていてほしいか
  • 迷ったら useState。形が同じなので、あとから移すのは 1 行
  • Context は置き場所ではなく、配る仕組み

compare.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useQueryState } from "nuqs";
import { useState } from "react";
import useLocalStorageState from "use-local-storage-state";

export function Compare() {
  useTrackDemoRender();

  // 3 つとも同じ形。違うのは置き場所だけ
  const [inMemory, setInMemory] = useState("");
  const [inUrl, setInUrl] = useQueryState("memo", { defaultValue: "" });
  const [inStorage, setInStorage] = useLocalStorageState(
    "react-lesson-compare",
    { defaultValue: "" },
  );

  return (
    <div className="flex flex-col gap-4">
      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">useState(メモリ)</span>
        <Input
          value={inMemory}
          onChange={(event) => setInMemory(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">useQueryState(URL)</span>
        <Input
          value={inUrl}
          onChange={(event) => setInUrl(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <label className="flex flex-col gap-1.5">
        <span className="text-muted-foreground">
          useLocalStorageState(ブラウザ)
        </span>
        <Input
          value={inStorage}
          onChange={(event) => setInStorage(event.target.value)}
          placeholder="何か入力"
        />
      </label>

      <Button
        size="sm"
        variant="outline"
        onClick={() => window.location.reload()}
      >
        3 つとも入力してから、ここを押して再読み込み
      </Button>
    </div>
  );
}