黑白梦黑白梦

toggle navtoggle nav
  • 文章
  • 专栏
  • 文章
  • 专栏

React Query 项目实践

发布于 2026-02-19, 更新于 2026-02-20

简介

React Query(现名为 TanStack Query)是一个用于 React 应用的服务端状态管理库。它主要解决了以下问题:

  • 服务端状态与客户端状态分离:服务端数据(Server State)由后端拥有,客户端只是借用;而 UI 状态(Client State)由客户端拥有。
  • 缓存管理:自动去重请求,缓存数据。
  • 后台更新:在数据过期(Stale)后,自动在后台重新获取(Revalidate)。
  • 性能优化:窗口聚焦重新获取、网络恢复重新获取、分页/无限加载等。

核心理念是 SWR (Stale-While-Revalidate) :先返回缓存中的陈旧数据(Stale),同时在后台发起请求获取最新数据(Revalidate),拿到后更新 UI。

核心 API

useQuery (读数据)

用于获取数据:

  • queryKey: 唯一标识符,依赖变化时自动触发重新请求。
  • queryFn: 返回 Promise 的异步函数。
  • staleTime: 数据多久变为陈旧(默认 0ms,即立即过期)。
  • enabled: 是否自动运行(依赖项检查)。

useMutation (写数据)

用于创建/更新/删除数据:

  • mutationFn: 执行副作用的异步函数。
  • onSuccess: 成功回调,常用于使相关 Query 失效(Invalidation)以触发刷新。

useQueryClient (全局操作)

用于获取 QueryClient 实例,操作缓存:

  • invalidateQueries: 标记查询失效,强制重新获取。
  • setQueryData: 手动更新缓存(乐观更新)。
  • removeQueries: 直接移除匹配的缓存数据。

清除缓存

区别概览:

  • **invalidateQueries**:把匹配的 query 标记为过期(stale),保留缓存数据;如果该 query 正在被组件使用,会触发重新请求
  • **removeQueries**:直接把匹配的 query 从缓存里移除;下次使用时等同于“第一次加载”

何时用哪一个:

  • 数据改动后希望 UI 自动刷新但保留旧数据显示过渡:用 invalidateQueries
  • 需要彻底清空缓存(如退出登录、切换账号、权限变更):用 removeQueries

在管理端的直觉用法:

  • 一般写操作(新增/编辑/删除):invalidateQueries
  • 登出或用户切换:removeQueries

项目实践

项目主要在 管理后台 使用 React Query。

全局配置 (Provider)

项目在 QueryProvider 中配置了 React Query,并采用了 Next.js App Router 推荐的单例模式。

  • 客户端保持单例,复用缓存。
  • 设置 staleTime: 60 * 1000 (1分钟),减少不必要的后台刷新
// QueryProvider.tsx
'use client';

import { isServer, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactNode } from 'react';

// 创建 Client 实例的工厂函数
function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000, // 默认缓存 1 分钟
      },
    },
  });
}

let browserQueryClient: QueryClient | undefined = undefined;

function getQueryClient() {
  if (isServer) {
    // 服务端预渲染,为每个请求创建一个全新的 QueryClient 实例
    return makeQueryClient();
  } else {
    // 客户端使用单例模式,复用缓存
    if (!browserQueryClient) browserQueryClient = makeQueryClient();
    return browserQueryClient;
  }
}

export default function QueryProvider({ children }: { children: ReactNode }) {
  const client = getQueryClient();
  return (
    <QueryClientProvider client={client}>
      {children}
    </QueryClientProvider>
  );
}

数据获取 (Query)

以文章列表为例,项目结合 useSearchParams 和 zod Schema 来管理查询参数,并将其作为 queryKey 的一部分。

流程:

  1. 从 URL 获取查询参数(page, count, status 等)。
  2. 使用 zod 校验参数。
  3. 构建 queryKey,包含所有依赖项。
  4. 传递给 useQuery。
// AdminPostsPage.tsx
'use client';

import { useQuery } from '@tanstack/react-query';
import { useSearchParams } from 'next/navigation';
import { useMemo } from 'react';
import { fetchAdminPosts } from '@/app/admin/_api';

export default function AdminPostsPage() {
  const searchParams = useSearchParams();

  // 1. 解析并校验 URL 参数
  const query = useMemo(() => {
    return {
      page: Number(searchParams.get('page') || 1),
      count: Number(searchParams.get('count') || 20),
      status: searchParams.get('status') ? Number(searchParams.get('status')) : undefined,
      // ...其他参数
    };
  }, [searchParams]);

  // 2. 发起查询
  const posts = useQuery({
    // Query Key: 包含所有筛选条件,任意变动都会触发重新请求
    queryKey: ['admin', 'posts', query.page, query.count, query.status],
    // Query Function: 调用 API
    queryFn: () => fetchAdminPosts(query),
  });

  if (posts.isLoading) return <div>加载中...</div>;
  
  return (
    <div>
      {/* 渲染列表 */}
      {posts.data?.data?.items.map(post => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
}

数据变更 (Mutation)

以保存文章为例,使用 useMutation 提交数据,并在成功后刷新相关缓存。

流程:

  1. 使用 useQueryClient 获取实例。
  2. mutationFn 执行 API 调用。
  3. onSuccess 中调用 invalidateQueries,使 'admin', 'posts' 相关的查询失效,从而触发列表和详情页的自动刷新。
// PostEditor.tsx
'use client';

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { updateAdminPost, createAdminPost } from '@/app/admin/_api';

export default function PostEditor({ initial }) {
  const queryClient = useQueryClient();

  // 定义 Mutation
  const saveMutation = useMutation({
    mutationFn: (payload) =>
      payload.id ? updateAdminPost(payload.id, payload.data) : createAdminPost(payload.data),
    
    onSuccess: (data, variables) => {
      // 成功后刷新缓存
      // 1. 刷新文章列表
      queryClient.invalidateQueries({ queryKey: ['admin', 'posts'] });
      // 2. 刷新特定文章详情(如果是更新)
      if (variables.id) {
        queryClient.invalidateQueries({ queryKey: ['admin', 'posts', variables.id] });
      }
      // 3. 刷新仪表盘统计
      queryClient.invalidateQueries({ queryKey: ['admin', 'dashboard'] });
    },
  });

  const onSubmit = async (data) => {
    try {
      await saveMutation.mutateAsync({ id: initial.id, data });
      alert('保存成功');
    } catch (error) {
      alert('保存失败');
    }
  };

  return (
    <button onClick={() => onSubmit({ title: '新标题' })} disabled={saveMutation.isPending}>
      {saveMutation.isPending ? '保存中...' : '保存'}
    </button>
  );
}

API 封装

我们在 src/app/admin/_api/index.ts 中封装了所有 API 请求,统一处理 Cookie 凭证(credentials: 'include')和错误抛出。

// api/index.ts
async function jsonRequest(url: string, init: RequestInit) {
  const res = await fetch(url, {
    ...init,
    credentials: 'include', // 携带 Cookie (Session)
    headers: { 'content-type': 'application/json' },
  });
  const json = await res.json().catch(() => null);
  if (!res.ok) throw new Error(json?.error?.message || '请求失败');
  return json;
}

export async function fetchAdminPosts(params) {
  // ...构建 URLSearchParams
  return jsonRequest(`/api/admin/posts?${params}`, { method: 'GET' });
}

项目实践总结

  1. Query Key 必须唯一且确定:像 ['admin', 'posts', id] 这样的数组结构非常清晰,方便精确控制缓存失效。
  2. Server vs Client:始终在 Client Component ('use client') 中使用 Hooks,但在 Provider 层要做好 SSR 的兼容处理。
  3. URL 是事实来源:将状态(如分页、筛选)同步到 URL,不仅方便分享,也能天然地配合 useQuery 的依赖机制。

©2015-2026 黑白梦 粤ICP备15018165号

联系: heibaimeng@foxmail.com