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 に似せて作られているからです。 新しい置き場所を覚えるたびに、新しい書き方を覚え直す必要はありません。
全部に入力してから、再読み込みしてみる
読み込み中…
再読み込みすると、はっきり分かれます。
- メモリ … 消えます
- 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>
);
}