状態管理ライブラリの種類
クライアント状態を管理するもの — Zustand、Jotai、Redux)
サーバー状態を管理するもの —SWR、React Query、Apollo
SWRとは ReactのデータフェッチングをSWRでシンプルにする
SWR は Vercel 製の、データ取得用の React Hooks ライブラリ。
どういうケースで使用するか
サーバーが正のデータを画面に表示するロジック
- サーバー上のデータを画面に表示する読み取り系の処理(ユーザー情報、記事一覧、通知件数など)
- 複数のコンポーネントで同じデータを使いたいとき
- 頻繁に変わるデータをrefreshさせたいとき
- useEffect + useState で書いていた処理
useEffect(() => {
const fetchUser = async () => {
try {
const res = await fetch(`/api/user/${id}`)
setUser(await res.json())
} catch (e) {
setError(e)
} finally {
setIsLoading(false)
}
}
fetchUser()
}, [id])
const fetcher = async (url) => {
const res = await fetch(url)
if (!res.ok) throw new Error('取得に失敗しました')
return res.json()
}
const { data: user, error, isLoading } = useSWR(`/api/user/${id}`, fetcher)
向いていないケース
クライアント内で完結する状態。フォームの入力値、モーダルの開閉、タブの選択状態。これらは useState やクライアント用のステート管理の領域
基本的な使い方
https://swr.vercel.app/ja/docs/getting-started
const { data, error, isLoading } = useSWR(`/api/user/${id}`, fetcher)上記の記述でuseSWR関数(カスタムフック)を呼び出します
ちなみに useSWR 自体をラップして自分のカスタムフックを作るのはよくあるパターン。
第1引数: key
キャッシュのキーであり、多くの場合そのままリクエスト先の識別子になる。
- 同じキーなら、どのコンポーネントから呼んでも同じキャッシュを共有する。10箇所で
useSWR('/api/user/1')を呼んでもリクエストは1回
デフォルトキャッシュは。消えるのはブラウザのフルリロード・タブを閉じたとき(=JS のメモリが破棄されたとき)だけです。 - 値が変わると新しいデータとして再フェッチが走る。
idが変われば自動的に取り直される nullまたはundefinedを返す関数を渡すとフェッチをスキップする(条件付きフェッチ)- 文字列以外に配列やオブジェクトも使える。配列の場合は各要素が fetcher に個別の引数として渡される
第2引数: fetcher
key を受け取ってデータを返す非同期関数。Promise を返しさえすれば中身は自由。
data
fetcher が解決した値。まだ取得できていない間は undefined。キャッシュがあれば即座に古い値が入り、裏で再検証が走る(stale-while-revalidate)。
error
fetcher が throw / reject した場合にその例外が入る。何もなければ undefined。重要なのは、fetcher が自分でエラーを投げないと error に入らないこと。fetch は 404 や 500 でも reject しないので、fetcher 側で明示的に throw する必要がある。
sLoading
「リクエスト進行中で、まだ表示できるデータがない」状態。SWR v2 で追加された。似たものに isValidating があり、こちらはキャッシュがある状態での再検証中も true になる。初回のスケルトン表示には isLoading、更新中のスピナーには isValidating を使い分ける。
この戦略の動作は単純で、まずキャッシュにあるデータを即座に返し(stale)、裏側でAPIリクエストを飛ばし(revalidate)、最新データが取れたら画面を差し替える。ユーザーから見ると、画面遷移のたびにローディングスピナーが出ることなく、常にデータが表示されている状態になる。
useEffect + useStateで自前管理していたデータフェッチングのボイラープレートを、useSWRフック一つに置き換えられる。キャッシュ管理、再検証のタイミング制御、エラーハンドリング、重複リクエストの排除までライブラリ側が面倒を見てくれる。
SWRのデータ取得フロー
SWRがリクエストを処理する流れを整理する。
キャッシュがある場合は古いデータを即座に表示してからバックグラウンドで再取得する。キャッシュがない初回アクセスではisLoadingがtrueになり、通常のローディング状態を経てからデータが表示される。
useSWRの第1引数は「キー」で、キャッシュの識別子かつfetcherに渡される値になる。第2引数のfetcherはデータを取得する関数で、Promiseを返せば何でもよい。fetchに限らず、axiosでもGraphQLクライアントでも使える。
返り値のdataはフェッチ結果、errorはスローされたエラー、isLoadingはデータもキャッシュもない初回読み込み中かどうかを示す。
グローバル設定
fetcherを毎回渡すのは面倒なので、SWRConfigでアプリ全体のデフォルトを設定できる。
import { SWRConfig } from 'swr'
const fetcher = (url: string) => fetch(url).then(res => res.json())
function App() {
return (
<SWRConfig value={{ fetcher }}>
<Dashboard />
</SWRConfig>
)
}
これ以降、各コンポーネントのuseSWRではキーだけ渡せばよい。
function Dashboard() {
const { data } = useSWR('/api/dashboard')
// ...
}
Next.js を使っている場合の注意
App Router の Server Components を使っているなら、初回表示のデータ取得はサーバー側でやったほうがいい。SWR が価値を発揮するのは、その後のクライアント側での更新・再検証・共有の部分。全部を SWR でやろうとすると、サーバーで取れば済むものをわざわざクライアントから2往復させることになる。
SWR を使う = そのデータについては RSC を使っていない。
理由は単純で、SWR はクライアントコンポーネント専用だから。use client が付いた場所でしか動かず、そこからはサーバーの DB を直接触れない。だから Route Handler を立てて HTTP で取りにいく。
言い換えると、データの取得経路は2択。
- RSC で取る → サーバー内で直接。Route Handler も SWR も不要
- SWR で取る → クライアントから Route Handler 経由

キャッシュと再検証の仕組み
SWRのキャッシュはインメモリで管理される。同じキーに対するuseSWRは、コンポーネントが別の場所にあっても同じキャッシュを共有する。これにより、ヘッダーのユーザー名とプロフィールページのユーザー情報が常に同期される。
再検証が走るタイミングはデフォルトで3つある。
| トリガー | 説明 | オプション |
|---|---|---|
| フォーカス時 | ブラウザタブに戻ったとき | revalidateOnFocus |
| ネットワーク復帰時 | オフラインからオンラインに戻ったとき | revalidateOnReconnect |
| マウント時 | コンポーネントがマウントされたとき | revalidateOnMount |
不要な再検証を抑えたい場合は個別にオフにできる。
const { data } = useSWR('/api/settings', fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
})
ポーリング
リアルタイム性が求められるデータには、refreshIntervalでポーリングを設定できる。
const { data } = useSWR('/api/notifications', fetcher, {
refreshInterval: 3000, // 3秒ごとに再取得
})
ミューテーションと楽観的更新
データの読み取りだけでなく、更新後にキャッシュを反映させる仕組みも用意されている。mutate関数でキャッシュを書き換えられる。
基本的なミューテーション
import useSWR, { mutate } from 'swr'
async function updateUser(newName: string) {
await fetch('/api/user', {
method: 'PUT',
body: JSON.stringify({ name: newName }),
})
// APIリクエスト後にキャッシュを再検証
mutate('/api/user')
}
楽観的更新
APIのレスポンスを待たずにUIを先に更新し、失敗時にロールバックするパターンも書ける。
import useSWRMutation from 'swr/mutation'
function UserProfile() {
const { data } = useSWR('/api/user', fetcher)
const { trigger } = useSWRMutation('/api/user', updateUser)
async function handleUpdate(newName: string) {
await trigger(newName, {
optimisticData: { ...data, name: newName },
rollbackOnError: true,
})
}
return <div>{data?.name}</div>
}
async function updateUser(url: string, { arg }: { arg: string }) {
return fetch(url, {
method: 'PUT',
body: JSON.stringify({ name: arg }),
}).then(res => res.json())
}
optimisticDataで即座にUIに反映し、rollbackOnError: trueでAPIが失敗した場合に元のデータに戻す。ユーザーはラグを感じずに操作でき、エラー時も整合性が保たれる。
条件付きフェッチ
キーにnullを渡すか、関数でnullを返すと、SWRはリクエストを送らない。依存するデータが揃ってから取得したいケースで使う。
function UserPosts({ userId }: { userId: string | null }) {
// userIdがnullのときはフェッチしない
const { data } = useSWR(
userId ? `/api/users/${userId}/posts` : null,
fetcher
)
return <div>{data?.length ?? 0}件の投稿</div>
}
依存フェッチにも応用できる。
function UserDashboard() {
const { data: user } = useSWR('/api/user', fetcher)
// userが取得できてから、そのIDでpostsを取得
const { data: posts } = useSWR(
user ? `/api/users/${user.id}/posts` : null,
fetcher
)
// ...
}
無限ローディング(ページネーション)
useSWRInfiniteを使うと、ページネーションや無限スクロールを実装できる。
import useSWRInfinite from 'swr/infinite'
function PostList() {
const getKey = (pageIndex: number, previousPageData: any[]) => {
if (previousPageData && previousPageData.length === 0) return null
return `/api/posts?page=${pageIndex + 1}&limit=10`
}
const { data, size, setSize, isLoading } = useSWRInfinite(getKey, fetcher)
const posts = data ? data.flat() : []
const isEnd = data && data[data.length - 1]?.length < 10
return (
<div>
{posts.map(post => (
<article key={post.id}>{post.title}</article>
))}
{!isEnd && (
<button onClick={() => setSize(size + 1)}>
もっと読む
</button>
)}
</div>
)
}
getKey関数がページごとのキーを生成する。前のページのデータが空配列ならnullを返してフェッチを止める。setSizeでページ数を増やすと、次のページを取得する。
エラーハンドリングとリトライ
SWRはデフォルトでエラー時にリトライする。リトライ間隔は指数バックオフで増えていく。
const { data, error } = useSWR('/api/data', fetcher, {
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
// 404はリトライしない
if (error.status === 404) return
// 最大10回まで
if (retryCount >= 10) return
// 5秒後にリトライ
setTimeout(() => revalidate({ retryCount }), 5000)
},
})
onErrorRetryでリトライのロジックを完全にカスタマイズできる。ステータスコードに応じてリトライを止めたり、間隔を調整したりする。
SWRとTanStack Queryの使い分け
SWRと同じ領域のライブラリとしてTanStack Query(旧React Query)がある。どちらを選ぶかは、プロジェクトの性質によって変わる。
| 観点 | SWR | TanStack Query |
|---|---|---|
| バンドルサイズ | 約4KB(gzip) | 約13KB(gzip) |
| APIの設計思想 | シンプル、最小限 | 網羅的、多機能 |
| ミューテーション | mutate + 手動管理 | useMutationで体系的に管理 |
| Devtools | なし | 専用のDevtoolsあり |
| キャッシュのGC | なし(手動管理) | gcTimeで自動管理 |
| 学習コスト | 低い | やや高い |
SWRはデータの読み取りが中心で、ミューテーションが少ないプロジェクトに向いている。ダッシュボード、ブログのフロント、設定画面など。TanStack Queryはミューテーションが多く、キャッシュのライフサイクルを細かく管理したいプロジェクトに向いている。管理画面、ECサイト、フォームの多いアプリなど。
軽量に始めたいならSWR、最初から複雑なキャッシュ戦略が見えているならTanStack Queryという判断でよい。
実践で使うためのヒント
SWRを導入するなら、まずSWRConfigでfetcherとエラーハンドリングのデフォルトをアプリ全体に設定するところから始めるとよい。個別のコンポーネントで毎回fetcherを渡す書き方は、プロジェクトが大きくなるとすぐに面倒になる。
カスタムフックにする習慣もつけておきたい。useSWR('/api/user', fetcher)を直接コンポーネントに書くのではなく、useUserのようなフックに抽出すれば、キーの変更やオプションの追加が一箇所で済む。
function useUser() {
const { data, error, isLoading } = useSWR('/api/user')
return {
user: data,
isLoading,
isError: error,
}
}
TypeScriptを使っているなら、fetcherの返り値の型をジェネリクスで指定できる。
const { data } = useSWR<User>('/api/user')
// data の型は User | undefined
既存プロジェクトへの導入は段階的に進められる。既存のuseEffectによるデータフェッチングを、影響の小さいコンポーネントから1つずつuseSWRに置き換えていけばよい。全体を一度に書き換える必要はない。

react zustand
React向けの非常にシンプルで軽量な状態管理ライブラリです。
Reduxのように複雑な設定が必要なく、「ただのJavaScriptオブジェクトを作る感覚」で扱えます
createで作成したものが「ストア」
ストア、グローバルステートとは
そもそもストアとはグローバルステートを保存するためのオブジェクトや仕組みになります
グローバルステートとは、useStateで管理するような特定のコンポーネント内でのみではなく、アプリのどこからでもアクセスできるデータ
createは、storeを作成して、そのstoreにアクセスするためのフックを返します。
storeの初期状態と更新関数を定義したオブジェクトを記述します
簡単なサンプル
import { create } from 'zustand';
const useStore = create((set) => ({
// 状態(State)
count: 0,
// 更新関数(Action)
inc: () => set((state) => ({ count: state.count + 1 })),
}));