mcpField
R
react-query

react-query

github.com/mindrally开发2871.5k 次安装3 周前更新
代码仓库

关于 react-query

面向 React 与 TypeScript 开发者的 React Query(现称 TanStack Query)实践指南,介绍服务端数据获取与缓存模式。内容还涉及错误处理、状态管理配合及性能优化。

主要功能

  1. 01查询缓存与状态管理
  2. 02依赖查询、分页与无限滚动
  3. 03变更处理与乐观更新
  4. 04查询键、预取与选择性订阅
  5. 05错误处理与开发工具

适用场景

  1. 01组织功能模块中的查询与变更钩子
  2. 02实现分页列表或无限滚动数据
  3. 03在写入后更新或失效缓存
  4. 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 处理所有数据获取和缓存
  • 利用 React Query 的内置状态管理,而不是用 useState 管理服务端数据
  • 使用 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>
  );
}

查询模式

基本查询 Hook
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>
  );
}

关键约定

  1. 使用 React Query DevTools 检查缓存并跟踪查询状态
  2. 将 react-query hooks 放在特定功能的目录中(按功能组织)
  3. 始终使用用户友好的消息和重试选项妥善处理错误
  4. 仅获取所需数据——使用 API 参数减少数据传输
  5. 避免查询深度嵌套;尽可能扁平化以提升性能
  6. 组件专属数据使用本地状态,共享数据使用全局状态
  7. 充分利用 React Query 内置的缓存和状态管理功能

应避免的反模式

  • 不要使用 useEffect 获取数据
  • 不要将服务端数据存储在 useState 中
  • 不要忘记处理加载状态和错误状态
  • 不要创建缺少恰当缓存失效策略的查询
  • 条件查询不要漏掉 enabled 选项
  • 不要忽略查询响应的 TypeScript 类型

评论

支持 Markdown