react-query

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Query Best Practices

React Query 最佳实践

You are an expert in React Query, TypeScript, and React development. React Query (now TanStack Query) simplifies data fetching logic with built-in caching, background updates, and stale data management.
您是React Query、TypeScript和React开发领域的专家。React Query(现更名为TanStack Query)通过内置的缓存、后台更新和过期数据管理功能简化了数据获取逻辑。

Core Principles

核心原则

  • Use React Query for all data fetching and caching
  • Leverage React Query's built-in state management instead of
    useState
    for server data
  • Use React Context and
    useReducer
    for managing client-side global state
  • Avoid excessive API calls through proper caching strategies
  • Always handle loading states and errors properly
  • 所有数据获取和缓存操作都使用React Query
  • 利用React Query的内置状态管理来处理服务端数据,而非使用
    useState
  • 使用React Context和
    useReducer
    管理客户端全局状态
  • 通过合理的缓存策略避免过多的API调用
  • 始终妥善处理加载状态和错误

Project Structure

项目结构

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
src/
  components/
    [Feature]/
      index.tsx
      queries.ts           # 功能专属的查询Hook
      mutations.ts         # 功能专属的突变Hook
  hooks/
    useAuth.ts
    useApi.ts
  services/
    api/
      client.ts            # Axios/fetch 配置
      users.ts             # 用户API函数
      posts.ts             # 文章API函数
  providers/
    ReactQueryProvider.tsx
  types/
    index.ts

Setup

配置步骤

Provider Configuration

提供者配置

typescript
// 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>
  );
}
typescript
// 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分钟
      cacheTime: 30 * 60 * 1000,   // 30分钟
      retry: 2,
      refetchOnWindowFocus: true,
    },
  },
});

export function ReactQueryProvider({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools />
    </QueryClientProvider>
  );
}

Query Patterns

查询模式

Basic Query Hook

基础查询Hook

typescript
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
    }
  );
}
typescript
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分钟
    }
  );
}

Query with Error Handling

带错误处理的查询

Services should throw user-friendly errors that React Query can catch and display:
typescript
// 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} />;
}
服务端应抛出用户友好的错误,以便React Query捕获并展示:
typescript
// services/api/users.ts
export async function fetchUser(userId: string): Promise<User> {
  const response = await fetch(`/api/users/${userId}`);

  if (!response.ok) {
    // 抛出用户友好的错误信息
    throw new Error('无法加载用户资料,请重试。');
  }

  return response.json();
}

// 组件使用示例
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} />;
}

Dependent Queries

依赖查询

typescript
function useUserWithPosts(userId: string) {
  const userQuery = useUser(userId);

  const postsQuery = useQuery(
    ['posts', userId],
    () => fetchUserPosts(userId),
    {
      enabled: !!userQuery.data,
    }
  );

  return { userQuery, postsQuery };
}
typescript
function useUserWithPosts(userId: string) {
  const userQuery = useUser(userId);

  const postsQuery = useQuery(
    ['posts', userId],
    () => fetchUserPosts(userId),
    {
      enabled: !!userQuery.data,
    }
  );

  return { userQuery, postsQuery };
}

Paginated Queries

分页查询

typescript
function usePaginatedUsers(page: number, limit: number = 10) {
  return useQuery(
    ['users', 'list', { page, limit }],
    () => fetchUsers({ page, limit }),
    {
      keepPreviousData: true,
    }
  );
}
typescript
function usePaginatedUsers(page: number, limit: number = 10) {
  return useQuery(
    ['users', 'list', { page, limit }],
    () => fetchUsers({ page, limit }),
    {
      keepPreviousData: true,
    }
  );
}

Infinite Scroll

无限滚动

typescript
import { useInfiniteQuery } from 'react-query';

function useInfiniteUsers() {
  return useInfiniteQuery(
    ['users', 'infinite'],
    ({ pageParam = 1 }) => fetchUsers({ page: pageParam }),
    {
      getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
    }
  );
}
typescript
import { useInfiniteQuery } from 'react-query';

function useInfiniteUsers() {
  return useInfiniteQuery(
    ['users', 'infinite'],
    ({ pageParam = 1 }) => fetchUsers({ page: pageParam }),
    {
      getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
    }
  );
}

Mutation Patterns

突变模式

Basic Mutation

基础突变

typescript
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);
    },
  });
}
typescript
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);
    },
  });
}

Optimistic Updates

乐观更新

typescript
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]);
    },
  });
}
typescript
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]);
    },
  });
}

State Management Integration

状态管理集成

Combining with Context/Reducer

与Context/Reducer结合使用

Use React Query for server state and Context/Reducer for client state:
typescript
// 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} />;
}
使用React Query管理服务端状态,使用Context/Reducer管理客户端状态:
typescript
// 客户端状态(使用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>
  );
}

// 服务端状态(使用React Query)
function UserDashboard() {
  const { theme } = useAppState();         // 客户端状态
  const { data: user } = useUser(userId);  // 服务端状态

  return <Dashboard theme={theme} user={user} />;
}

Combining with Zustand (Alternative)

与Zustand结合使用(替代方案)

typescript
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} />;
}
typescript
import { create } from 'zustand';

// 客户端状态存储
const useStore = create((set) => ({
  theme: 'light',
  setTheme: (theme) => set({ theme }),
}));

// 同时使用两者的组件
function App() {
  const theme = useStore((state) => state.theme);
  const { data: user } = useUser(userId);

  return <Layout theme={theme} user={user} />;
}

Performance Optimization

性能优化

Query Key Best Practices

查询键最佳实践

typescript
// 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,
  },
};
typescript
// 结构化查询键
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,
  },
};

Selective Subscriptions

选择性订阅

typescript
// Only subscribe to user name changes
function useUserName(userId: string) {
  return useUser(userId, {
    select: (user) => user.name,
  });
}
typescript
// 仅订阅用户名称的变化
function useUserName(userId: string) {
  return useUser(userId, {
    select: (user) => user.name,
  });
}

Prefetching

预获取

typescript
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>
  );
}
typescript
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>
  );
}

Error Handling Patterns

错误处理模式

Global Error Handler

全局错误处理器

typescript
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      onError: (error: Error) => {
        console.error('Query error:', error);
      },
    },
    mutations: {
      onError: (error: Error) => {
        toast.error(error.message);
      },
    },
  },
});
typescript
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      onError: (error: Error) => {
        console.error('Query error:', error);
      },
    },
    mutations: {
      onError: (error: Error) => {
        toast.error(error.message);
      },
    },
  },
});

Error Boundaries

错误边界

typescript
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>
  );
}
typescript
import { QueryErrorResetBoundary } from 'react-query';
import { ErrorBoundary } from 'react-error-boundary';

function App() {
  return (
    <QueryErrorResetBoundary>
      {({ reset }) => (
        <ErrorBoundary
          onReset={reset}
          fallbackRender={({ error, resetErrorBoundary }) => (
            <div>
              <p>出现错误:{error.message}</p>
              <button onClick={resetErrorBoundary}>重试</button>
            </div>
          )}
        >
          <UserProfile />
        </ErrorBoundary>
      )}
    </QueryErrorResetBoundary>
  );
}

Key Conventions

关键约定

  1. Use React Query DevTools to inspect cache and track query status
  2. Group react-query hooks within feature-specific directories (feature-based organization)
  3. Always handle errors properly with user-friendly messages and retry options
  4. Fetch only required data - use API parameters to reduce data transfer
  5. Avoid deeply nesting queries - flatten when possible for better performance
  6. Use local state for component-specific data, global state for shared data
  7. Leverage React Query's built-in caching and state management capabilities
  1. 使用React Query DevTools检查缓存并跟踪查询状态
  2. 将React Query Hook按功能分组到专属目录中(基于功能的组织方式)
  3. 始终通过用户友好的消息和重试选项妥善处理错误
  4. 仅获取所需数据 - 使用API参数减少数据传输
  5. 避免深度嵌套查询 - 尽可能扁平化以提升性能
  6. 使用本地状态存储组件专属数据,使用全局状态存储共享数据
  7. 充分利用React Query的内置缓存和状态管理能力

Anti-Patterns to Avoid

需要避免的反模式

  • Do not use
    useEffect
    for data fetching
  • Do not store server data in
    useState
  • Do not forget loading and error state handling
  • Do not create queries without proper cache invalidation strategies
  • Do not skip the
    enabled
    option for conditional queries
  • Do not ignore TypeScript types for query responses
  • 不要使用
    useEffect
    进行数据获取
  • 不要将服务端数据存储在
    useState
  • 不要忘记处理加载和错误状态
  • 不要在没有合适缓存失效策略的情况下创建查询
  • 不要跳过条件查询的
    enabled
    选项
  • 不要忽略查询响应的TypeScript类型