react-query について
React と TypeScript の開発者向けに、React Query(現 TanStack Query)でのサーバーデータ取得とキャッシュの実践方法を解説します。エラー処理、状態管理との使い分け、性能調整も扱います。
主な機能
- 01クエリキャッシュと状態管理
- 02依存クエリ、ページネーション、無限スクロール
- 03ミューテーションと楽観的更新
- 04クエリキー、事前取得、選択的購読
- 05エラー処理と開発者ツール
活用シーン
- 01機能単位でクエリ・ミューテーションフックを整理
- 02ページネーションや無限スクロールを実装
- 03書き込み後にキャッシュを更新または無効化
- 04読み込み、エラー、キャッシュ状態を確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ mindrally/ skills --skill react-queryインストール後、Claude Code で/react-queryを入力して読み込まれたか確認します。
詳細
React Query のベストプラクティス
あなたは React Query、TypeScript、React 開発のエキスパートです。React Query(現在の TanStack Query)は、組み込みのキャッシュ、バックグラウンド更新、古くなったデータの管理により、データ取得ロジックを簡素化します。
基本原則
- すべてのデータ取得とキャッシュに React Query を使用します
- サーバーデータには
useStateではなく、React Query に組み込まれた状態管理を活用します - クライアント側のグローバル状態の管理には、React Context と
useReducerを使用します - 適切なキャッシュ戦略によって、過剰な API 呼び出しを避けます
- ローディング状態とエラーを必ず適切に処理します
プロジェクト構成
src/
components/
[Feature]/
index.tsx
queries.ts # Feature-specific query hooks
mutations.ts # Feature-specific mutation hooks
hooks/
useAuth.ts
useApi.ts
services/
api/
client.ts # Axios/fetch configuration
users.ts # User API functions
posts.ts # Post API functions
providers/
ReactQueryProvider.tsx
types/
index.ts
セットアップ
Provider の設定
// providers/ReactQueryProvider.tsx
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 30 * 60 * 1000, // 30 minutes
retry: 2,
refetchOnWindowFocus: true,
},
},
});
export function ReactQueryProvider({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools />
</QueryClientProvider>
);
}
クエリのパターン
基本的なクエリフック
import { useQuery } from 'react-query';
import { fetchUser, User } from '@/services/api/users';
export function useUser(userId: string) {
return useQuery<User, Error>(
['user', userId],
() => fetchUser(userId),
{
enabled: !!userId,
staleTime: 1000 * 60 * 10, // 10 minutes
}
);
}
エラー処理を伴うクエリ
サービスでは、React Query が捕捉して表示できるように、ユーザーにわかりやすいエラーをスローする必要があります。
// services/api/users.ts
export async function fetchUser(userId: string): Promise<User> {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
// Throw user-friendly error message
throw new Error('Unable to load user profile. Please try again.');
}
return response.json();
}
// Component usage
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading, error } = useUser(userId);
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error.message} />;
return <ProfileCard user={user} />;
}
依存関係のあるクエリ
function useUserWithPosts(userId: string) {
const userQuery = useUser(userId);
const postsQuery = useQuery(
['posts', userId],
() => fetchUserPosts(userId),
{
enabled: !!userQuery.data,
}
);
return { userQuery, postsQuery };
}
ページネーション付きクエリ
function usePaginatedUsers(page: number, limit: number = 10) {
return useQuery(
['users', 'list', { page, limit }],
() => fetchUsers({ page, limit }),
{
keepPreviousData: true,
}
);
}
無限スクロール
import { useInfiniteQuery } from 'react-query';
function useInfiniteUsers() {
return useInfiniteQuery(
['users', 'infinite'],
({ pageParam = 1 }) => fetchUsers({ page: pageParam }),
{
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
}
);
}
ミューテーションのパターン
基本的なミューテーション
import { useMutation, useQueryClient } from 'react-query';
function useCreateUser() {
const queryClient = useQueryClient();
return useMutation(createUser, {
onSuccess: () => {
queryClient.invalidateQueries(['users']);
},
onError: (error: Error) => {
toast.error(error.message);
},
});
}
楽観的更新
function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation(updateUser, {
onMutate: async (updatedUser) => {
await queryClient.cancelQueries(['user', updatedUser.id]);
const previousUser = queryClient.getQueryData(['user', updatedUser.id]);
queryClient.setQueryData(['user', updatedUser.id], updatedUser);
return { previousUser };
},
onError: (err, updatedUser, context) => {
if (context?.previousUser) {
queryClient.setQueryData(['user', updatedUser.id], context.previousUser);
}
},
onSettled: (data, error, updatedUser) => {
queryClient.invalidateQueries(['user', updatedUser.id]);
},
});
}
状態管理との統合
Context/Reducer との組み合わせ
サーバー状態には React Query を使用し、クライアント状態には Context/Reducer を使用します。
// Client state with Context
const AppStateContext = createContext<AppState | undefined>(undefined);
const AppDispatchContext = createContext<Dispatch<Action> | undefined>(undefined);
function AppProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(appReducer, initialState);
return (
<AppStateContext.Provider value={state}>
<AppDispatchContext.Provider value={dispatch}>
{children}
</AppDispatchContext.Provider>
</AppStateContext.Provider>
);
}
// Server state with React Query
function UserDashboard() {
const { theme } = useAppState(); // Client state
const { data: user } = useUser(userId); // Server state
return <Dashboard theme={theme} user={user} />;
}
Zustand との組み合わせ(代替案)
import { create } from 'zustand';
// Client state store
const useStore = create((set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}));
// Component using both
function App() {
const theme = useStore((state) => state.theme);
const { data: user } = useUser(userId);
return <Layout theme={theme} user={user} />;
}
パフォーマンスの最適化
クエリキーのベストプラクティス
// Structured query keys
const queryKeys = {
users: {
all: ['users'] as const,
lists: () => [...queryKeys.users.all, 'list'] as const,
list: (filters: Filters) => [...queryKeys.users.lists(), filters] as const,
details: () => [...queryKeys.users.all, 'detail'] as const,
detail: (id: string) => [...queryKeys.users.details(), id] as const,
},
};
選択的なサブスクリプション
// Only subscribe to user name changes
function useUserName(userId: string) {
return useUser(userId, {
select: (user) => user.name,
});
}
プリフェッチ
function UserListItem({ userId }: { userId: string }) {
const queryClient = useQueryClient();
const handleMouseEnter = () => {
queryClient.prefetchQuery(
['user', userId],
() => fetchUser(userId),
{ staleTime: 60000 }
);
};
return (
<li onMouseEnter={handleMouseEnter}>
<Link to={`/users/${userId}`}>View Profile</Link>
</li>
);
}
エラー処理のパターン
グローバルエラーハンドラー
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: (error: Error) => {
console.error('Query error:', error);
},
},
mutations: {
onError: (error: Error) => {
toast.error(error.message);
},
},
},
});
エラーバウンダリー
import { QueryErrorResetBoundary } from 'react-query';
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>Something went wrong: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
>
<UserProfile />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}
主な規約
- React Query DevTools を使ってキャッシュを調べ、クエリの状態を追跡します
- react-query のフックは、機能ごとのディレクトリ内にまとめます(機能ベースの構成)
- ユーザーにわかりやすいメッセージと再試行オプションを用いて、エラーを必ず適切に処理します
- 必要なデータだけを取得し、API パラメーターを使ってデータ転送量を減らします
- クエリを深くネストすることは避け、パフォーマンス向上のため、可能な場合はフラット化します
- コンポーネント固有のデータにはローカル状態を、共有データにはグローバル状態を使用します
- React Query に組み込まれたキャッシュと状態管理の機能を活用します
避けるべきアンチパターン
- データ取得に
useEffectを使用しないでください - サーバーデータを
useStateに保存しないでください - ローディング状態とエラー状態の処理を忘れないでください
- 適切なキャッシュ無効化戦略なしにクエリを作成しないでください
- 条件付きクエリでは
enabledオプションを省略しないでください - クエリレスポンスの TypeScript 型を無視しないでください
コメント
Markdown 対応