Part 9 · state を共有する
53 / 64 章
サーバーのデータ
取得したデータは state とは別物として扱う
最後の置き場所はサーバーです。 そしてこれだけは、これまでと性質が違います。
これまでの 3 つは、自分が持ち主でした。 サーバーのデータは違います。持ち主は向こうにいて、こちらが持っているのは写しです。
その違いが、扱い方を変えます。
自分で書くと、state が増えていく
Part 6 でやった useEffect を使えば、 データの取得そのものは書けます。
動くが、state が 3 つ必要になっている
読み込み中…
Part 8 の Suspense で一度出した形です。 動きます。ですが、state が 3 つ必要になりました。
const [members, setMembers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);しかもまだ足りません。 実際のアプリでは、こういう要求が次々に来ます。
- 「取り直す」ボタンを付けたい
- 同じデータを別の画面でも使いたい(また取りに行く?)
- 連続で押されたとき、古い結果が後から届いて上書きするのを防ぎたい
- タブに戻ってきたら最新にしたい
- 失敗したら数回やり直したい
これを全部、正しく書くのは大変です。そしてアプリのどこでデータを取るときも、同じ苦労を繰り返します。
manual-fetch.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";
/*
自分で書くとこうなる、という例。
lint はこの書き方を「描き直しが連鎖する」として止めてくる。
それも含めて「自前で正しく書くのは大変だ」という話なので、
このファイルだけ黙らせている。
*/
/* eslint-disable react-hooks/set-state-in-effect */
export function ManualFetch() {
useTrackDemoRender();
// 取得したデータのために、state が 3 つも必要になる
const [members, setMembers] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setIsLoading(true);
fetch("/api/members")
.then((res) => res.json())
.then((data) => setMembers(data.members))
.catch(() => setError("取得に失敗しました"))
.finally(() => setIsLoading(false));
}, []);
return (
<div className="flex flex-col gap-3">
{isLoading && <p className="text-muted-foreground">読み込み中…</p>}
{error && <p className="text-destructive">{error}</p>}
{!isLoading && !error && (
<ul className="flex flex-wrap gap-2">
{members.map((member) => (
<li key={member} className="rounded-md border px-3 py-1.5">
{member}
</li>
))}
</ul>
)}
<Button size="sm" variant="outline" disabled>
取り直す(自分で書くと、ここも作り込みが要る)
</Button>
</div>
);
}取得は道具に任せる
そこで、データ取得専用の道具を使います。デモでは SWR を使っています。
const { data, error, isLoading, mutate } = useSWR("/api/members", fetcher);第 2 引数の fetcher は、取ってくる処理そのものです。自分で書きます。
const fetcher = (url) => fetch(url).then((res) => res.json());「道具を入れたのに、なぜ自分で書くのか」と思うかもしれません。 SWR が引き受けるのはいつ・何回・どう取り直すかのほうで、取ってくる手段は使う側が決めるという作りだからです。 だから GraphQL でも、別のライブラリでも、そのまま乗せられます。
この 1 行に、さっきの state 3 つが全部入っています。さらに mutate(取り直す)まで付いてきます。
取り直すボタンが動く。押すたびに違う人が返る
読み込み中…
さきほど挙げた要求も、ほとんどが最初から入っています。 タブに戻ったときの再取得も、同じ URL の重複防止も、自分では書きません。
with-swr.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import useSWR from "swr";
// 404 や 500 でも fetch は成功扱いになる。
// ここで throw しておかないと、失敗が error に入ってこない
const fetcher = async (url: string) => {
const response = await fetch(url);
if (!response.ok) throw new Error("取得に失敗しました");
return response.json();
};
export function WithSwr() {
useTrackDemoRender();
// 読み込み中も、失敗も、取り直しも、これ 1 行に含まれている
const { data, error, isLoading, mutate } = useSWR<{ members: string[] }>(
"/api/members",
fetcher,
);
return (
<div className="flex flex-col gap-3">
{isLoading && <p className="text-muted-foreground">読み込み中…</p>}
{error && <p className="text-destructive">取得に失敗しました</p>}
{data && (
<ul className="flex flex-wrap gap-2">
{data.members.map((member) => (
<li key={member} className="rounded-md border px-3 py-1.5">
{member}
</li>
))}
</ul>
)}
<Button size="sm" variant="outline" onClick={() => mutate()}>
取り直す
</Button>
</div>
);
}state として持たない
ここが、この章でいちばん大事なところです。
// ✕ 取ってきたデータを state に写す
const { data } = useSWR("/api/members", fetcher);
const [members, setMembers] = useState([]);
useEffect(() => {
if (data) setMembers(data.members);
}, [data]);Part 6 でやった「props を state に写す」とまったく同じ間違いです。 写した瞬間に二重管理が始まります。
サーバーのデータは、こちらが持ち主ではありません。いつでも取り直せる「写し」として扱います。 自分の state にしてしまうと、 向こうが変わったことに気づけなくなります。
with-swr.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import useSWR from "swr";
// 404 や 500 でも fetch は成功扱いになる。
// ここで throw しておかないと、失敗が error に入ってこない
const fetcher = async (url: string) => {
const response = await fetch(url);
if (!response.ok) throw new Error("取得に失敗しました");
return response.json();
};
export function WithSwr() {
useTrackDemoRender();
// 読み込み中も、失敗も、取り直しも、これ 1 行に含まれている
const { data, error, isLoading, mutate } = useSWR<{ members: string[] }>(
"/api/members",
fetcher,
);
return (
<div className="flex flex-col gap-3">
{isLoading && <p className="text-muted-foreground">読み込み中…</p>}
{error && <p className="text-destructive">取得に失敗しました</p>}
{data && (
<ul className="flex flex-wrap gap-2">
{data.members.map((member) => (
<li key={member} className="rounded-md border px-3 py-1.5">
{member}
</li>
))}
</ul>
)}
<Button size="sm" variant="outline" onClick={() => mutate()}>
取り直す
</Button>
</div>
);
}ほかの選択肢
- TanStack Query … SWR と同じ役割で、より多機能。大きなアプリで選ばれることが多い
- Server Component … Next.js なら、そもそもサーバー側でデータを取って 完成した画面を送れます。取得の状態管理自体がなくなります
// Server Component なら、これだけで済む
async function MemberList() {
const members = await getMembers();
return <ul>{members.map(...)}</ul>;
}読み込み中も、失敗も、二重管理も出てきません。まずこれで済まないかを考えるのが、いまの標準的な進め方です。
SWR や TanStack Query が要るのは、ブラウザ側で取り直したいときです。 検索、無限スクロール、定期更新など、操作に応じて何度も取りに行く場面になります。
with-swr.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import useSWR from "swr";
// 404 や 500 でも fetch は成功扱いになる。
// ここで throw しておかないと、失敗が error に入ってこない
const fetcher = async (url: string) => {
const response = await fetch(url);
if (!response.ok) throw new Error("取得に失敗しました");
return response.json();
};
export function WithSwr() {
useTrackDemoRender();
// 読み込み中も、失敗も、取り直しも、これ 1 行に含まれている
const { data, error, isLoading, mutate } = useSWR<{ members: string[] }>(
"/api/members",
fetcher,
);
return (
<div className="flex flex-col gap-3">
{isLoading && <p className="text-muted-foreground">読み込み中…</p>}
{error && <p className="text-destructive">取得に失敗しました</p>}
{data && (
<ul className="flex flex-wrap gap-2">
{data.members.map((member) => (
<li key={member} className="rounded-md border px-3 py-1.5">
{member}
</li>
))}
</ul>
)}
<Button size="sm" variant="outline" onClick={() => mutate()}>
取り直す
</Button>
</div>
);
}理解できたか確かめる
確認クイズ
サーバーから取ってきたデータを useState に入れるべき?
確認クイズ
useSWR のキーが同じだと何が起きる?
with-swr.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import useSWR from "swr";
// 404 や 500 でも fetch は成功扱いになる。
// ここで throw しておかないと、失敗が error に入ってこない
const fetcher = async (url: string) => {
const response = await fetch(url);
if (!response.ok) throw new Error("取得に失敗しました");
return response.json();
};
export function WithSwr() {
useTrackDemoRender();
// 読み込み中も、失敗も、取り直しも、これ 1 行に含まれている
const { data, error, isLoading, mutate } = useSWR<{ members: string[] }>(
"/api/members",
fetcher,
);
return (
<div className="flex flex-col gap-3">
{isLoading && <p className="text-muted-foreground">読み込み中…</p>}
{error && <p className="text-destructive">取得に失敗しました</p>}
{data && (
<ul className="flex flex-wrap gap-2">
{data.members.map((member) => (
<li key={member} className="rounded-md border px-3 py-1.5">
{member}
</li>
))}
</ul>
)}
<Button size="sm" variant="outline" onClick={() => mutate()}>
取り直す
</Button>
</div>
);
}この章のまとめ
- サーバーのデータは持ち主が向こうにある「写し」
- 自分で書くと、読み込み中・失敗・取り直し・重複防止を 全部作り込むことになる
- SWR などの道具に任せると、その全部が 1 行に収まる
- 取ってきたデータを state に写さない。二重管理になる
- Next.js なら、まずServer Component で済まないかを考える
with-swr.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import useSWR from "swr";
// 404 や 500 でも fetch は成功扱いになる。
// ここで throw しておかないと、失敗が error に入ってこない
const fetcher = async (url: string) => {
const response = await fetch(url);
if (!response.ok) throw new Error("取得に失敗しました");
return response.json();
};
export function WithSwr() {
useTrackDemoRender();
// 読み込み中も、失敗も、取り直しも、これ 1 行に含まれている
const { data, error, isLoading, mutate } = useSWR<{ members: string[] }>(
"/api/members",
fetcher,
);
return (
<div className="flex flex-col gap-3">
{isLoading && <p className="text-muted-foreground">読み込み中…</p>}
{error && <p className="text-destructive">取得に失敗しました</p>}
{data && (
<ul className="flex flex-wrap gap-2">
{data.members.map((member) => (
<li key={member} className="rounded-md border px-3 py-1.5">
{member}
</li>
))}
</ul>
)}
<Button size="sm" variant="outline" onClick={() => mutate()}>
取り直す
</Button>
</div>
);
}