🛳️

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側で「保存が成功したら一覧を更新する」みたいな処理を書かなくて良いのが感動的✨

画面間でのキャッシュの共有

useQueryuseMutationを掛け合わせることで画面間でのキャッシュの共有ができる。

これは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