Part 9 · state を共有する
51 / 64 章
URL に state を置く
リロードしても消えない状態
状態を置く場所は、useState だけではありません。
検索条件や、選んでいるタブ。 こういうものを useState に入れると、リロードで消え、URL を送っても相手には伝わりません。
置き場所をURL にすると、それが全部解決します。
書き方は useState とほぼ同じ
// いつもの
const [keyword, setKeyword] = useState("");
// 置き場所を URL にしたもの
const [keyword, setKeyword] = useQueryState("keyword", { defaultValue: "" });形はまったく同じです。 受け取るものも、更新のしかたも変わりません。 変わったのは、値がどこに保管されているかだけです。
打つたびにアドレス欄が変わる
読み込み中…
入力して、次の 3 つを試してみてください。
- アドレス欄を見る …
?keyword=…が付いています - リロードする … 入力が残っています
- URL をコピーして別のタブで開く … 同じ絞り込みの状態で開きます
url-state.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";
const ALL = ["りんご", "みかん", "ぶどう", "もも", "いちご"];
export function UrlState() {
useTrackDemoRender();
// useState とほとんど同じ形。置き場所が URL になっただけ
const [keyword, setKeyword] = useQueryState("keyword", { defaultValue: "" });
const found = ALL.filter((item) => item.includes(keyword));
return (
<div className="flex flex-col gap-4">
<Input
placeholder="果物を絞り込む"
aria-label="果物を絞り込む"
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
/>
<ul className="flex flex-wrap gap-2">
{found.map((item) => (
<li key={item} className="rounded-md border px-3 py-1.5">
{item}
</li>
))}
</ul>
<Button size="sm" variant="outline" onClick={() => setKeyword("")}>
クリア
</Button>
<p className="text-muted-foreground">
打つたびに、ブラウザのアドレス欄が変わります
</p>
</div>
);
}URL に置くと何が起きるか
URL はブラウザが元から持っている状態の置き場所です。 そこに乗せるだけで、ブラウザの機能がそのまま使えるようになります。
- リロードしても消えない
- URL を送れば、同じ画面を相手に見せられる
- 戻る / 進むボタンを効かせることもできる(後述)
- お気に入りに登録できる
とくに 2 つめが大きい。「この検索結果を見てほしい」というとき、URL を貼るだけで済みます。useState ではこうはいきません。
url-state.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";
const ALL = ["りんご", "みかん", "ぶどう", "もも", "いちご"];
export function UrlState() {
useTrackDemoRender();
// useState とほとんど同じ形。置き場所が URL になっただけ
const [keyword, setKeyword] = useQueryState("keyword", { defaultValue: "" });
const found = ALL.filter((item) => item.includes(keyword));
return (
<div className="flex flex-col gap-4">
<Input
placeholder="果物を絞り込む"
aria-label="果物を絞り込む"
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
/>
<ul className="flex flex-wrap gap-2">
{found.map((item) => (
<li key={item} className="rounded-md border px-3 py-1.5">
{item}
</li>
))}
</ul>
<Button size="sm" variant="outline" onClick={() => setKeyword("")}>
クリア
</Button>
<p className="text-muted-foreground">
打つたびに、ブラウザのアドレス欄が変わります
</p>
</div>
);
}素で書くと面倒なので、道具を使う
URL の読み書きは、自分でやると意外と手間がかかります。
- 文字列なので、数値や真偽値は自分で変換する
- 値がないときの既定値を毎回書く
- 更新するときに他のパラメータを壊さないよう気をつける
デモで使っている nuqs は、これをuseState と同じ形に包んでくれるライブラリです。 型変換も面倒を見てくれます。
// 数値として扱う
const [page, setPage] = useQueryState("page", parseAsInteger.withDefault(1));
// 真偽値として扱う
const [open, setOpen] = useQueryState("open", parseAsBoolean.withDefault(false));Suspense で包む必要がある(Next.js 固有の話)
Next.js でこれを使うと、ビルド時にエラーが出ることがあります。
Error occurred prerendering page "/lessons/url-state"Next.js は、ページをあらかじめ組み立てておくことがあります。 ですがその時点では URL がまだ決まっていません。 クエリ文字列は、実際にアクセスされて初めて分かるものだからです。
そこで、URL を読む部品を Suspense で包み、「ここは後から埋まる」と React に伝えます。
<Suspense fallback={<p>読み込み中…</p>}>
<SearchBox />
</Suspense>このデモも、そうやって包んであります。useSearchParams を直接使う場合も同じ対処が必要です。
url-state.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";
const ALL = ["りんご", "みかん", "ぶどう", "もも", "いちご"];
export function UrlState() {
useTrackDemoRender();
// useState とほとんど同じ形。置き場所が URL になっただけ
const [keyword, setKeyword] = useQueryState("keyword", { defaultValue: "" });
const found = ALL.filter((item) => item.includes(keyword));
return (
<div className="flex flex-col gap-4">
<Input
placeholder="果物を絞り込む"
aria-label="果物を絞り込む"
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
/>
<ul className="flex flex-wrap gap-2">
{found.map((item) => (
<li key={item} className="rounded-md border px-3 py-1.5">
{item}
</li>
))}
</ul>
<Button size="sm" variant="outline" onClick={() => setKeyword("")}>
クリア
</Button>
<p className="text-muted-foreground">
打つたびに、ブラウザのアドレス欄が変わります
</p>
</div>
);
}理解できたか確かめる
確認クイズ
検索キーワードを URL に置く利点は?
確認クイズ
次のうち、URL に置くべきなのはどれ?
確認クイズ
検索窓の文字を URL に入れるとき、履歴はどう扱う?
url-state.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";
const ALL = ["りんご", "みかん", "ぶどう", "もも", "いちご"];
export function UrlState() {
useTrackDemoRender();
// useState とほとんど同じ形。置き場所が URL になっただけ
const [keyword, setKeyword] = useQueryState("keyword", { defaultValue: "" });
const found = ALL.filter((item) => item.includes(keyword));
return (
<div className="flex flex-col gap-4">
<Input
placeholder="果物を絞り込む"
aria-label="果物を絞り込む"
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
/>
<ul className="flex flex-wrap gap-2">
{found.map((item) => (
<li key={item} className="rounded-md border px-3 py-1.5">
{item}
</li>
))}
</ul>
<Button size="sm" variant="outline" onClick={() => setKeyword("")}>
クリア
</Button>
<p className="text-muted-foreground">
打つたびに、ブラウザのアドレス欄が変わります
</p>
</div>
);
}この章のまとめ
- 状態の置き場所は
useStateだけではない - URL に置くと、リロードで消えず、URL を送れば共有できる
- 判断の目安は「他人に見せて意味があるか」
- nuqs のような道具を使えば、書き方は
useStateと同じ形になる
url-state.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";
const ALL = ["りんご", "みかん", "ぶどう", "もも", "いちご"];
export function UrlState() {
useTrackDemoRender();
// useState とほとんど同じ形。置き場所が URL になっただけ
const [keyword, setKeyword] = useQueryState("keyword", { defaultValue: "" });
const found = ALL.filter((item) => item.includes(keyword));
return (
<div className="flex flex-col gap-4">
<Input
placeholder="果物を絞り込む"
aria-label="果物を絞り込む"
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
/>
<ul className="flex flex-wrap gap-2">
{found.map((item) => (
<li key={item} className="rounded-md border px-3 py-1.5">
{item}
</li>
))}
</ul>
<Button size="sm" variant="outline" onClick={() => setKeyword("")}>
クリア
</Button>
<p className="text-muted-foreground">
打つたびに、ブラウザのアドレス欄が変わります
</p>
</div>
);
}