効果的なReact Queryのキー

Published

これは、Dominik Dorfmeister 氏のブログ記事であるEffective React Query Keysを日本語訳してみたものです。

誤訳などあればIssueや PR を頂けると幸いです。


クエリのキーは React Query において非常に重要な核となる概念です。 これは、クエリーの依存に変化があった時、React Query が内部的にデータを正しくキャッシュしたり、自動的に再取得できるようにするために必要なことです。 そして、例えば、ミューテーションの後のデータ更新や、何かしらのクエリーをキャッシュ無効化する必要がある時、必要に応じて手動でクエリーのキャッシュとやりとりできるようにします。

より効果的にこのようなことを行う上で私がクエリーのキーをどのように構成しているかをお話しする前に、これら 3 つのポイントが意味することを確認してみましょう。

データのキャッシュ

内部的には、クエリーのキャッシュは、キーがクエリーキーをシリアライズしたものであり、値がクエリーのデータにメタ情報を加えた、ただの JavaScript のオブジェクトです。 キーは、決定論的な方法でハッシュ化されるので、オブジェクトを使うこともできます(トップレベルにおいてはキーが文字列か配列でなければいけないですが)。

最も重要な部分は、キーがクエリに対してユニークでなければいけないということです。React Query がキャッシュの中からキーに対するエントリーを見つけたら、それを使います。useQueryuseInfiniteQueryとで同じキーを使用できないことにも注意してください。結局のところ、ただ 1 つのクエリーキャッシュがあり、これら 2 つの間でデータは共有されるのです。無限のクエリーが”通常”のクエリーと根本的に異なる構造を持つため、これは好ましくありません。

dont-mix-keys

_10
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
_10
_10
// 🚨 これはうまくいきません
_10
useInfiniteQuery({ queryKey: ['todos'], queryFn: fetchInfiniteTodos })
_10
_10
// ✅ 代わりに他のものを選びましょう
_10
useInfiniteQuery({
_10
queryKey: ['infiniteTodos'],
_10
queryFn: fetchInfiniteTodos,
_10
})

自動的な再取得

クエリーは宣言的です。

これは強調しきれないほどとても重要な概念であり、また、”腑に落ちる”までしばらく時間を要するものです。ほとんどの人はクエリーを、特に再取得について、命令的な方法で考えます。

例えばクエリーが 1 つあり、何かデータを取得するとします。ボタンをクリックすると異なるパラメーターでデータを再取得しましょう。そうすると以下のような実装をよく目にします:

imperative-refetch

_10
function Component() {
_10
const { data, refetch } = useQuery({
_10
queryKey: ['todos'],
_10
queryFn: fetchTodos,
_10
})
_10
_10
// ❓ どのようにして再取得にパラメーターを渡すのでしょう ❓
_10
return <Filters onApply={() => refetch(???)} />
_10
}

答えはこうです: やらない。

refetchはこのためのものではありません。同じパラメーターで再取得するためのものです。

React Query がキーの変化するたび常時自動的に再取得をトリガーするため、データを変更する何か状態を持つ場合、必要な唯一のことはクエリーキーにそれを含めることです。そのため、絞り込みを適用したい場合、クライアントステートを変更するだけです:

query-key-drives-the-query

_10
function Component() {
_10
const [filters, setFilters] = React.useState()
_10
const { data } = useQuery({
_10
queryKey: ['todos', filters],
_10
queryFn: () => fetchTodos(filters),
_10
})
_10
_10
// ✅ ローカルの状態を設定して、クエリーを”駆動”します
_10
return <Filters onApply={setFilters} />
_10
}

setFiltersによって引き起こされる再レンダリングは、React Query に異なるクエリーキーを渡します。

手動でのやりとり

クエリーキャッシュとの手動でのやりとりは、クエリーキーの構造が最も重要なものです。 Query FiltersをサポートするinvalidateQueriessetQueriesDataのようなメソッドの多くがクエリーキーにあいまいな一致を許容します。

効果的な React Query のキー

これらの点は私の個人的な意見(実のところ、このブログの全てと同様)を反映しているので、クエリーキーを扱う時に絶対守らなければならないこととは考える必要がないことに留意してください。 この戦略はアプリケーションがより複雑になったとき最も効果的で、またとても良くスケールします。Todo アプリでは絶対に必要ありません 😁。

コロケーション

Kent C. Dodds氏によるMaintainability through colocationをまだ読んだことがなければ、読んでみてください。私は/src/utils/queryKeys.tsに全てのクエリーキーを保持することが状況を良くするとは考えていません。以下のように、私はそれぞれのクエリーのそばにクエリーキーを持って、機能のディレクトリにコロケーションします:


_10
- src
_10
- features
_10
- Profile
_10
- index.tsx
_10
- queries.ts
_10
- Todos
_10
- index.tsx
_10
- queries.ts

queriesファイルは React Query に関する全てを含むようにします。大抵カスタムフックだけをエクスポートし、クエリー関数とクエリーキーとがローカルに留まるようにします。

常に配列のキーを使用する

クエリーキーを文字列にもできますが、一貫性を持たせるため、私は常に配列にすることを好みます。React Query は内部的にいずれにしろ配列に変換します:

always-use-array-keys

_10
// 🚨 いずれにせよ['todos']に変換されます
_10
useQuery({ queryKey: 'todos' })
_10
// ✅
_10
useQuery({ queryKey: ['todos'] })

アップデート: React Query v4 では、全てのキーが配列でなければいけません。

構成

クエリーキーは、最も一般的なものから最も具体的なものへと、間を適切な粒度のレベルで、構成します。ここでは、私が絞り込み可能なリスト、並びに詳細の表示が可能な TODO リストをどのように構成するかを示します:


_10
['todos', 'list', { filters: 'all' }]
_10
['todos', 'list', { filters: 'done' }]
_10
['todos', 'detail', 1]
_10
['todos', 'detail', 2]

このような構成により、['todos']と紐づく全ての TODO、全てのリストか全ての詳細、正確なキーを知っていれば特定の 1 つをキャッシュ無効化することもできます。必要に応じて全てのリストを対象にできるので、ミューテーションのレスポンスからの更新がより柔軟になります:

updates-from-mutation-responses

_14
function useUpdateTitle() {
_14
return useMutation({
_14
mutationFn: updateTitle,
_14
onSuccess: (newTodo) => {
_14
// ✅ TODOの詳細を更新
_14
queryClient.setQueryData(['todos', 'detail', newTodo.id], newTodo)
_14
_14
// ✅ このTODOを含む全てのリストを更新
_14
queryClient.setQueriesData(['todos', 'list'], (previous) =>
_14
previous.map((todo) => (todo.id === newTodo.id ? newtodo : todo))
_14
)
_14
},
_14
})
_14
}

リストと詳細の構成が大きく異なるのであればうまくいかないかもしれませんが、代わりに、全てのリストをただキャッシュ無効化することももちろん可能です:

invalidate-all-lists

_11
function useUpdateTitle() {
_11
return useMutation({
_11
mutationFn: updateTitle,
_11
onSuccess: (newTodo) => {
_11
queryClient.setQueryData(['todos', 'detail', newTodo.id], newTodo)
_11
_11
// ✅ 全てのリストをキャッシュ無効化
_11
queryClient.invalidateQueries({ queryKey: ['todos', 'list'] })
_11
},
_11
})
_11
}

例えば URL から絞り込みするような、現在どのリストにいるか分かり、それゆえ正確なクエリーキーが構築できる場合、この 2 つのメソッドを組み合わせてリストに対してsetQueryDataを呼び、その他全てをキャッシュ無効化することもできます。

combine

_22
function useUpdateTitle() {
_22
// URLに保持している現在のフィルターを返すカスタムフックをイメージしてください
_22
const { filters } = useFilterParams()
_22
_22
return useMutation({
_22
mutationFn: updateTitle,
_22
onSuccess: (newTodo) => {
_22
queryClient.setQueryData(['todos', 'detail', newTodo.id], newTodo)
_22
_22
// ✅ 即座に現在のリストを更新
_22
queryClient.setQueryData(['todos', 'list', { filters }], (previous) =>
_22
previous.map((todo) => (todo.id === newTodo.id ? newtodo : todo))
_22
)
_22
_22
// 🥳 全てのリストをキャッシュ無効化しつつ、アクティブなものを再取得しない
_22
queryClient.invalidateQueries({
_22
queryKey: ['todos', 'list'],
_22
refetchActive: false,
_22
})
_22
},
_22
})
_22
}

アップデート: v4 で、refetchActiverefetchTypeに置き換わっています。上記の例だと何も再取得したくないので、refetchType: 'none'となります。

クエリーキーのファクトリーを使用する

上述の例で、たくさんのクエリーキーを手動で記述していることに気づいたでしょう。これは間違いの元であるだけでなく、例えばもう 1 段階他の粒度のキーを追加したくなった場合のような、将来の変更を難しくもします。

私が機能ごとのクエリーキーのファクトリーを推奨する理由がここにあります。これは、カスタムフックで利用できる、ただのエントリーとクエリーキーを生み出す関数のオブジェクトです。 上述の構造の例だと、このようになります:


_10
const todoKeys = {
_10
all: ['todos'] as const,
_10
lists: () => [...todoKeys.all, 'list'] as const,
_10
list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
_10
details: () => [...todoKeys.all, 'detail'] as const,
_10
detail: (id: number) => [...todoKeys.details(), id] as const,
_10
}

各階層が関連していながらも独立してアクセスできるようになっていて、とても柔軟です:

examples

_11
// 🕺 TODOの機能に関連する全てを削除
_11
queryClient.removeQueries({ queryKey: todoKeys.all })
_11
_11
// 🚀 全てのリストのキャッシュを無効化
_11
queryClient.invalidateQueries({ queryKey: todoKeys.lists() })
_11
_11
// 🙌 1つのTODOをプリフェッチ
_11
queryClient.prefetchQueries({
_11
queryKey: todoKeys.detail(id),
_11
queryFn: () => fetchTodo(id),
_11
})