Tanstack Query (ex. React Query)
原典
https://tanstack.com/query/latest/docs/react/overview
概要
- Server state(バックエンドのデータ)を fetching, caching, synchronizing and updating するためのライブラリ
- Server state は、ローカルの同期的なデータとは根本的に性質が異なる。にもかかわらず、redux のようなものでそれを管理してきたけど、辛いよね。私に頼りなさい。
インストール
yarn add react-query
// App.tsx
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
const queryClient = new QueryClient();
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools />
<MyApp />
</QueryClientProvider>
);
}
使い方
// MyComponent.tsx
import { useQuery } from 'react-query';
function MyComponent() {
const { isLoading, error, data } = useQuery('repoData', () =>
fetch('https://api.github.com/repos/tannerlinsley/react-query').then(
(res) => res.json(),
),
);
if (isLoading) return 'Loading...';
if (error) return 'An error has occurred: ' + error.message;
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{' '}
<strong>✨ {data.stargazers_count}</strong>{' '}
<strong>🍴 {data.forks_count}</strong>
</div>
);
}
デフォルト設定に関する注意
デフォルト設定を知っておかないと罠にはまるのでここはきちんと抑えておくこと。
- キャッシュデータは stale として扱われる(
staleTime
)。このため、下記の場合にデータが自動的に再取得される。- クエリを使用する新しいコンポーネントがマウントされた時(
refetchOnMount
) - ウィンドウが再フォーカスされた時(
refetchOnWindowFocus
) - ネットワークが再接続された時(
refetchOnReconnect
) - 再取得の間隔が明示的にセットされている時(
refetchInterval
)
- クエリを使用する新しいコンポーネントがマウントされた時(
- どのコンポーネントにも使用されていないキャッシュデータは
inactive
としてしばらく残るが、5 分後に削除される(cacheTime
)。 - 失敗したクエリは自動的に3回再試行される。間隔は指数関数的に伸びる。(
retry
,retryDelay
) - クエリ結果はStructural Sharingという仕組みで保持されている。これにより、本当に値が変わった時にだけ最小限のオブジェクトの参照が変更される。これは、ほとんどの場合で効率的である。
Query
- クエリするには下記の2つが必要
- ユニークなキー
- Promise(データを resolve するか、エラーを投げる)
const result = useQuery('todos', fetchTodoList);
// or
const result = useQuery({
queryKey: 'todos',
queryFn: fetchTodoList,
});
Status
Status はdata
に関する情報である。data
を持っているかどうかを示す。
isLoading
orstatus === 'loading'
データがまだ存在しない状態 (通信中かどうかは関係しないので注意)isError
orstatus === 'error'
エラーが発生した状態error
エラーの内容
isSuccess
orstatus === 'success'
データ取得が成功した状態data
データ
!isLoading && !isError
であることをチェックすれば、Type Narrowing が効くので、data
にアクセスできる。
FetchStatus
FetchStatus はqueryFn
に関する情報である。queryFn
が動作中かどうかを示す。
fetchStatus === 'fetching'
orisFetching
- 通信中であるfetchStatus === 'paused'
orisPaused
- 通信したいがオフラインなどの理由により保留中fetchStatus === 'idle'
- 通信していない
なぜ 2 つのステータスが必要だったのか
Background refetches と stale-while-revalidate の仕組みがあることにより、上記ステータスのあらゆる組み合わせが発生しうるため。
Query Key
- クエリキーに基づいてキャッシュが行われる
- シリアライズ可能な値ならなんでもキーとして使用できる
useQuery('todos', ...)
// queryKey === ['todos']
useQuery(['todo', 5], ...)
// queryKey === ['todo', 5]
useQuery(['todo', 5, { preview: true }], ...)
// queryKey === ['todo', 5, { preview: true }]
// なお、下記は同じものとして扱われる
useQuery(['todos', { status, page }], ...)
useQuery(['todos', { page, status }], ...)
クエリ関数が変数に依存している場合、例えば特定の id 等に基づいてクエリが実行されるなどの場合は、クエリキーにもその変数を含めること。
function Todos({ todoId }) {
const result = useQuery(['todos', todoId], () => fetchTodoById(todoId));
}
Query Functions
Query Functions = クエリを実行する(データを取得する)関数のこと。
- データ取得に失敗した時は必ずエラーを投げるもしくは
Promise.reject()
を返すこと。- そうすることで React Query は適切にエラーをハンドリングできる
axios
と異なり、fetch
はデフォルトではエラーを投げないので注意する
useQuery(['todos', todoId], async () => {
const { ok, json } = await fetch('/todos/' + todoId);
if (!ok) {
throw new Error('Network response was not ok');
}
return json();
});
- Query function にはコンテキストが渡されるので、必要に応じて使用するとよい
queryKey
や AbortSignal など
function Todos({ status, page }) {
const result = useQuery(['todos', { status, page }], fetchTodoList);
}
function fetchTodoList({ queryKey }) {
const [_key, { status, page }] = queryKey;
return new Promise();
}
Network Mode
3 つのモードがある
online
デフォルトはコレ
- ネットワーク接続があるとき
- クエリが実行される
- ネットワーク接続がないとき
- クエリは実行されず、Status の値は従前の
loading
,error
,success
の状態を維持する。 fetchStatus
はpending
になる
- クエリは実行されず、Status の値は従前の
- クエリ実行している最中にネットワーク接続を失ったとき
- リトライ機能は無効化される
- ただし
refetchOnReconnect
は実行される(これは正確には再取得というよりは継続というべき性質のものだから)
always
ネットワークを使用せず、AsyncStorage などで完結している場合に最適
- ネットワーク接続状態を考慮せず、常にクエリが実行される
fetchStatus
がpaused
になることはない- リトライが pause されることもなく、失敗時には直ちに
error
状態になる refetchOnReconnect
はデフォルトで無効化される
offlineFirst
(online と always の中間とのことだがちょっとよくわからん)
クエリを並列で実行する
useQuery
を並べて書けば OK- ただし、配列に基づいてクエリしたい場合や、suspense mode を使っている場合は
useQueries
を使う必要がある
const userQueries = useQueries(
users.map((user) => {
return {
queryKey: ['user', user.id],
queryFn: () => fetchUserById(user.id),
};
}),
)``;
クエリを直列に実行する
- クエリの実行結果を使って別のクエリを実行するには、
enabled
オプションを使用する。
// まずはユーザIDを取得する
const { data: user } = useQuery(['user', email], getUserByEmail);
const userId = user?.id;
// 次にユーザIDを使用してプロジェクトを取得する
const {
status,
fetchStatus,
data: projects,
} = useQuery(['projects', userId], getProjectsByUser, {
// userIDが存在する場合のみこのクエリを実行する
enabled: !!userId,
});
project の status, fetchStatus は以下の通り遷移することになる。
✅最初の状態
status: 'loading'
fetchStatus: 'idle'
✅ユーザの取得が終わって`enabled`が`true`になった時
status: 'loading'
fetchStatus: 'fetching'
✅projectが取得できたあと
status: 'success'
fetchStatus: 'idle'
バックグラウドでの通信をユーザに知らせるには
isFetching
を使えばよいisFetching
はリトライやリフェッチなどを含め、あらゆる通信でtrue
となるisLoading
は初回データ取得のとき(過去に 1 度もデータを取得できていない状態のとき)だけtrue
となる
- アプリ全体での通信状態を取得したい場合は
useIsFetching
を使用する
Window Focus Refetching
- デフォルトで有効になっている
- React Native で同様のことをしたい場合は AppState の active イベントにリスナーを登録する
- その他、詳細略
Disabling / Pausing Queries
enabled
オプションをfalse
設定することで以下のようになる。
- キャッシュデータが存在する場合は、
status === 'success'
状態になり、かつdata
が提供される - キャッシュデータがない場合は
status === 'loading
かつfetchStatus === 'idle'
状態になる - マウント時にクエリが実行されない
- バックグラウンドでリフェッチされない
invalidateQueries()
やrefetchQueries()
が発火されても再クエリしないrefetch()
を使って手動でクエリを実行することはできる
Query Retries
queryFn の失敗時にはデフォルトで 3 回リトライする
Pagenation
- 普通にページネーションしようとすると、切り替えのたびに
data
が空になったりloading
state になることで、画面がガタつくなど UI としてよろしくない挙動になる。 - これを防ぐには
keepPreviousData
オプションを有効にする。 - このオプションが無効だと:
- クエリ開始時には
status==='loading' && data === undefined
- クエリ開始時には