effectの回で、useEffectとfetchでデータを読みこみました。あのとき「実務ではライブラリに任せることが多い」と書いたまま先に進んでいたので、ここで回収します。
自前だと何が足りないのか
いまのuseBirds(Context版)は、これだけです。
useEffect(() => {
fetch('/birds.json')
.then((response) => response.json())
.then((data) => {
setBirds(data)
setIsLoading(false)
})
}, [])動きます。ただ、実務のアプリに必要なものがごっそり抜けています。
| 足りないもの | 自前で書くと |
|---|---|
| 失敗の扱い | .catchとエラー用のstateを足す |
| 再取得 | 「更新」ボタン、画面に戻ったときの取り直し |
| 重複リクエスト | 同じデータを2つの画面が使うと2回通信する |
| 競合状態 | 検索語を速く打つと、古い結果があとから届いて上書きする |
| キャッシュ | 一度見たデータを覚えておく |
| 再試行 | 通信が失敗したとき、少し待ってもう一度 |
全部書くとuseBirdsは100行を超えます。しかもアプリのどのデータ取得にも同じものが要ります。
TanStack Queryに置きかえる
デファクトスタンダードがTanStack Query(旧React Query)です。
npm install --save-exact @tanstack/react-queryアプリの上のほうを、専用のProviderで包みます。
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</BrowserRouter>
</StrictMode>,
)そして読み出しはuseQueryひとつになります。
import { useQuery } from '@tanstack/react-query'
import type { Bird } from '../types.ts'
async function fetchBirds(): Promise<Bird[]> {
const response = await fetch('/birds.json')
if (!response.ok) {
throw new Error('読みこめませんでした')
}
return response.json()
}
export function useBirds() {
const { data: birds = [], isPending, isError } = useQuery({
queryKey: ['birds'],
queryFn: fetchBirds,
})
return { birds, isPending, isError }
}useEffectもuseStateも消えました。 読みこみ中(isPending)も失敗(isError)も、最初から付いてきます。
Contextが要らなくなる
ここが今回いちばん大きい変化です。データ取得のために作ったContextは、まるごと削除できます。
contextの回でContextを入れた理由は「一覧ページと詳細ページで同じデータを共有したいから」でした。TanStack Queryではキャッシュそのものが共有の置き場なので、両方のページがuseBirds()を呼ぶだけで同じデータを見ます。
これまで: BirdsProvider(fetch+state)→ Context → 各ページ
これから: 各ページが useBirds() を呼ぶ → キャッシュが1つ更新はuseMutation
サーバーにデータを送るとき(追加・更新・削除)はuseMutationを使います。
const queryClient = useQueryClient()
const addBirdMutation = useMutation({
mutationFn: (newBird) => fetch('/api/birds', { method: 'POST', body: JSON.stringify(newBird) }),
onSuccess: () => {
// 成功したら一覧のキャッシュを「古い」印にする → 自動で取り直される
queryClient.invalidateQueries({ queryKey: ['birds'] })
},
})送る → 成功したらinvalidateQueries → 一覧が最新になる。この流れが実務での定番です。
このコースのアプリにはサーバーが無いので、キャッシュを直接書きかえる形にしておきます。
function addBird(newBird) {
queryClient.setQueryData(['birds'], (current = []) => [...current, newBird])
}入れるか、入れないか
小さなアプリに常に必要なわけではありません。
| 状況 | 判断 |
|---|---|
| 読みこみが1か所だけ・使い捨て | useEffect+fetchのままでよい |
| 同じデータを複数の画面で使う | 入れる |
| 検索・ページ送りで何度も取り直す | 入れる |
| 更新して一覧を最新にしたい | 入れる |
| Next.jsなどのフレームワークを使っている | フレームワーク側の仕組みを先に見る |
useEffectで自前に書いた回。「Effectは最後の手段」と書いた意味が、ここでつながります公式ドキュメントも見てみる
TanStack外部サイトQuick Start | TanStack Query React DocsTanStack Query公式のクイックスタート。QueryClientProvider・useQuery・useMutationの最小の形がまとまっています(英語)このレッスンのまとめ
- 自前の
useEffect+fetchに足りないのは、失敗・再取得・重複・競合状態・キャッシュ・再試行 - いちばん怖いのは行数ではなく、競合状態のような見つけにくいバグを自作すること
useQueryでuseEffectもuseStateも消える。isPending・isErrorが付いてくるqueryKeyはキャッシュの住所。キーに検索語を含めると、変わったときだけ取り直す- データ取得のためのContextは削除できる(テーマ等のContextは残る)
- 更新は
useMutation→invalidateQueries staleTimeを指定しないと、画面に戻るたびに取り直される
次は、増えすぎたフォームをライブラリに任せる回です。