- • APIから取得するデータ → React Query / SWR
- • 複数コンポーネントで共有するUI状態 → Zustand / Jotai
- • 単一コンポーネント内の状態 → useState
ReactのデータフェッチングをSWRでシンプルにする
SWRは、Vercelが開発したReact向けのデータフェッチングライブラリだ。名前はHTTPキャッシュ戦略の「stale-while-revalidate」に由来している。
この戦略の動作は単純で、まずキャッシュにあるデータを即座に返し(stale)、裏側でAPIリクエストを飛ばし(revalidate)、最新データが取れたら画面を差し替える。ユーザーから見ると、画面遷移のたびにローディングスピナーが出ることなく、常にデータが表示されている状態になる。
useEffect + useStateで自前管理していたデータフェッチングのボイラープレートを、useSWRフック一つに置き換えられる。キャッシュ管理、再検証のタイミング制御、エラーハンドリング、重複リクエストの排除までライブラリ側が面倒を見てくれる。
SWRのデータ取得フロー
SWRがリクエストを処理する流れを整理する。
キャッシュがある場合は古いデータを即座に表示してからバックグラウンドで再取得する。キャッシュがない初回アクセスではisLoadingがtrueになり、通常のローディング状態を経てからデータが表示される。
基本的な使い方
インストール
npm install swr
useSWRフックの基本形
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then(res => res.json())
function UserProfile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher)
if (error) return <div>エラーが発生しました</div>
if (isLoading) return <div>読み込み中...</div>
return <div>{data.name}のプロフィール</div>
}
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')
// ...
}
キャッシュと再検証の仕組み
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 })),
}));