TanStack Queryを使って感動した話
システム内容
- DBを持たない
- サブシステム的立ち位置
- メインシステムのAPIを叩いて情報を取得し表示
- 動的な情報と静的な情報を表示する必要がある(一部リアルタイム性を求める)
このように少し複雑な構成をしていて、「APIで成り立つフロント専用システム」的なものを作ることになった
あまりフロント側を触ったことがなかったので、新しい技術を習得する目的で、上記システム条件に合う技術スタックを調べているとTanStack Queryに出会う。
なぜTanStack Queryを選定したのか
TanStack Queryの強み
- グローバルステートを意識しないで良い
- APIの呼び出し回数が減る
- キャッシュデータのハンドリングが柔軟
- ページ間でキャッシュの共有が可能
- 条件付きフェッチが可能(任意のタイミングでデータを更新できる)
- キャッシュがAPIごとに紐ずく
TanStack Queryが生まれる前の問題点として「非同期のデータ管理」が大変だった。
全部自分でやろうとすると、エラー/成功の状態分岐の管理が複雑だったり、キャッシュや再取得のタイミングを自分で設計・管理する必要があった。
TanStack Queryの誕生後...
- 「データ取得 → キャッシュ → 画面での共有」まで 一気に安全にできる
- 複数コンポーネントで同じデータを使っても重複リクエストが走らない
- 状態管理(ローディング/エラー/成功)が標準化されている
- キャッシュや再取得のタイミングを制御できる
TanStack Queryが生まれる前までの懸念点をほとんどライブラリに任せられる(グローバルステートを意識しないで良い)
Queryオプションの紹介
- useQuery・・・データの取得
└ queryKey
└ queryFn
└ staleTime
└ gcTime - useMutation・・・データの作成・更新・削除
useQuery
useQueryはTanStack Queryの一番の強みであるQueryオプション
先ほども書いたようにTanStack Queryが生まれる前はAPIの状態(いつ取る?取れた?まだ?失敗した?)などをそれぞれ詳細に決める必要があった
そこでuseQueryを使用することで、「今のデータはこれ!」を返してくれるのでAPIの状態をいちいち管理する必要がなくなった。
useQueryを用いた一番基本的な形
js側での設定方法
import { useQuery } from '@tanstack/vue-query'
const fetchUsers = async () => {
const res = await fetch('/api/users')
return res.json()
}
export function useUsersQuery() {
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 1000 * 60,
gcTime: Infinity,
})
}
queryKey・・・このデータは[users]という名前にします宣言
queryFn・・・叩くAPI[fetchUsers]を指定する
staleTime・・・キャッシュの有効期限をmsで指定。これを過ぎると新しいデータを再取得する
gcTime・・・キャッシュの保持期限をmsで指定。これを過ぎると古いキャッシュが捨てられる
コンポーネント側での使い方
<script setup lang="ts">
import { useUsersQuery } from '@/queries/useUsersQuery'
const { data, isLoading, isError } = useUsersQuery()
</script>
<template>
<div v-if="isLoading">読み込み中...</div>
<div v-else-if="isError">エラーが発生しました</div>
<ul v-else>
<li v-for="user in data" :key="user.id">
{{ user.name }}
</li>
</ul>
</template>
useQueryはコンポーネント側から呼び出す時に本領を発揮する。
コンポーネント側での使い方は、useUsersQuery()を呼ぶだけ!
あとは、よしなに「データ取得 → キャッシュ → 画面での共有」まで全てやってくれる。
よって、「今のデータはこれ!」が実現できる
useMutation
useMutationはTanStack Queryを支えるもう一つのqueryオプション。
使い所としては、先ほどのuseQueryが「データの取得」なら、useMutationは「データの変更」
を担うオプション。
useMutationを用いた一番基本的な形
const updateUser = async (user) => {
const res = await fetch(`/api/users/${user.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(user),
})
return res.json()
}
import { useMutation, useQueryClient } from '@tanstack/vue-query'
export function useUpdateUserMutation() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: updateUser,
onSuccess: () => {
// 更新後に一覧を最新化
queryClient.invalidateQueries(['users'])
},
})
}
mutationFn・・・叩くAPI[updateUser]を指定する
onSuccess・・・APIが成功した後にやりたい処理
コンポーネント側での使い方
<template>
<button :disabled="isPending" @click="onClickSave">
保存
</button>
</template>
<script setup lang="ts">
import { useUpdateUserMutation } from '@/mutations/useUpdateUserMutation'
const { mutate, isPending } = useUpdateUserMutation()
const onClickSave = () => {
mutate({
id: 1,
name: '田中太郎',
})
}
</script>
useMutationの使い方もコンポーネントからmutate()を呼ぶだけ!
例で挙げた処理の内容は、mutateに入れられた引数をupdateUserに渡して、APIを実行するという流れ。
しかもonSuccessオプションで成功後の処理もかけるため、UI側で「保存が成功したら一覧を更新する」みたいな処理を書かなくて良いのが感動的✨
画面間でのキャッシュの共有
useQueryとuseMutationを掛け合わせることで画面間でのキャッシュの共有ができる。
これはTanStackの一番の強みである。
例えば。。。
よくある画面
画面A:ユーザー一覧画面
/users API を叩いて一覧表示
画面B:ユーザー編集モーダル
ユーザー名を編集して「保存」ボタンを押す
画面A:一覧画面(useQuery)
useQuery({
queryKey: ['users'], // この時点でキャッシュに['users']という名前を保存しておく
queryFn: fetchUsers,
})
画面B:編集画面(useMutation)
useMutation({
mutationFn: updateUser,
onSuccess: () => {
queryClient.invalidateQueries(['users'])
},
})
このように書くだけで、一覧画面の更新処理を書いていないのに、編集画面で変更された情報が一覧画面にも反映される。
画面間でのキャッシュの共有の一番のミソは
queryClient.invalidateQueries(['users'])
である。
こいつは何をやっているかというと、「users って名前のデータ、古くなったから取り直して!」をしている。
これをしてくれることで['users']キャッシュを使っている全ての画面を探し、それらの画面を自動的にリフェッチしてくれる。
これの何がすごいのか👇🏻
①画面間の依存を考えなくて良い
編集画面は一覧画面が存在するかどうかを知らなくて良いので、単純にデータを変更する処理だけを書けば良い
②更新漏れが起きない
inbalidateし忘れない限り指定の全画面を一気に最新にすることができる
③スケールが怖くない
同じ['users']を使う画面が10個あっても全部更新されるので、スケールしても問題ない
この「指定したqueryKeyを使っている全ての画面を、まとめて最新化してくれる」仕組みに感動したお話でした。
Discussion