用 useState 管理服务端数据?不如试试 React Query 来“避坑”

0 阅读7分钟

在写前端应用的时候,我发现自己经常重复着同样的模式:useEffect + fetch + useState。刚开始觉得这很自然,不就是请求数据、设置状态嘛。但随着项目变大,我越来越感觉不对劲:为什么到处都是重复的 loading、error 处理?为什么同一个用户信息要请求好几次?为什么数据更新后其他组件不同步?

直到接触了 React Query,我才恍然大悟:原来服务端数据和普通状态根本不是一回事。这篇文章是我重新理解这个问题的过程,也希望能帮到同样困惑的你。

一个完整的对比:用户资料页

让我们从一个实际场景开始。假设要做一个用户资料页,需要:

  • 显示用户信息(名字、头像等)
  • 支持编辑昵称
  • 处理加载状态和错误
  • 多个组件共享用户数据(Header 和 Profile 都需要)

听起来不复杂对吧?我们先看看用传统方式怎么写。

方案 1:用 useState(传统方式)

// 环境:React
// 场景:用传统方式管理服务端数据

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  // 获取数据
  useEffect(() => {
    setLoading(true);
    setError(null);
    
    fetch(`/api/users/${userId}`)
      .then(res => {
        if (!res.ok) throw new Error('Failed to fetch');
        return res.json();
      })
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, [userId]);
  
  // 更新昵称
  const updateName = async (newName) => {
    try {
      const res = await fetch(`/api/users/${userId}`, {
        method: 'PUT',
        headers: { 'Content-Type''application/json' },
        body: JSON.stringify({ name: newName }),
      });
      
      if (!res.ok) throw new Error('Update failed');
      
      const updatedUser = await res.json();
      setUser(updatedUser);  // 更新本地状态
      
    } catch (err) {
      alert('fail to update');
    }
  };
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return null;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={() => updateName('new name')}>
        Update Name
      </button>
    </div>
  );
}

看起来没问题?但等等,我们还需要在 Header 组件里显示用户名:

// Header 也需要用户信息,又要写一遍
function Header({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    // 又是一模一样的逻辑...
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);
  
  return <div>{user?.name}</div>;
}

这时候问题就来了:

❌ 问题 1:代码重复 Profile 和 Header 各自管理状态,同样的请求逻辑写了两遍。更糟的是,如果它们同时渲染,会发起两次相同的网络请求!

❌ 问题 2:缓存缺失 用户从 Profile 页跳到其他页,再跳回来,组件重新渲染 → useEffect 触发 → 又请求一遍。即使数据根本没变,用户也要看着 loading 转圈。

❌ 问题 3:数据同步困难 在 Profile 页改了昵称,更新了本地的 user 状态。但 Header 组件里的昵称怎么更新?它们是两个独立的 useState,互不影响!

❌ 问题 4:缺少错误重试 网络请求失败了,用户只能刷新页面重试。我们得手动实现一个"重试"按钮,还要管理重试次数、延迟等逻辑。

❌ 问题 5:缺少乐观更新 点击更新按钮 → 等待请求完成 → 界面才变化。用户会觉得卡顿,体验很差。

方案 2:用 React Query

现在让我们看看 React Query 如何解决这些问题:

// 环境:React + React Query
// 场景:用专业工具管理服务端数据
// 依赖:npm install @tanstack/react-query

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// 封装获取用户的函数
const fetchUser = async (userId) => {
  const res = await fetch(`/api/users/${userId}`);
  if (!res.ok) throw new Error('Failed to fetch');
  return res.json();
};

// 封装更新用户的函数
const updateUser = async ({ userId, data }) => {
  const res = await fetch(`/api/users/${userId}`, {
    method: 'PUT',
    headers: { 'Content-Type''application/json' },
    body: JSON.stringify(data),
  });
  if (!res.ok) throw new Error('Update failed');
  return res.json();
};

function UserProfile({ userId }) {
  const queryClient = useQueryClient();
  
  // 获取数据(自动处理 loading、error、缓存)
  const { data: user, isLoading, error, refetch } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 5 * 60 * 1000// 5分钟内认为数据是新鲜的
    retry: 3// 失败自动重试 3 次
  });
  
  // 更新数据(支持乐观更新、自动同步)
  const updateMutation = useMutation({
    mutationFn: updateUser,
    
    // 乐观更新:立即显示效果
    onMutate: async (newData) => {
      await queryClient.cancelQueries({ queryKey: ['user', userId] });
      const previousUser = queryClient.getQueryData(['user', userId]);
      
      queryClient.setQueryData(['user', userId], (old) => ({
        ...old,
        ...newData.data,
      }));
      
      return { previousUser };
    },
    
    // 失败时回滚
    onError: (err, newData, context) => {
      queryClient.setQueryData(['user', userId], context.previousUser);
      alert('fail to update');
    },
    
    // 成功后让缓存失效(确保数据最新)
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['user', userId] });
    },
  });
  
  const handleUpdateName = () => {
    updateMutation.mutate({
      userId,
      data: { name: 'new name' },
    });
  };
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return (
    <div>
      Error: {error.message}
      <button onClick={() => refetch()}>retry</button>
    </div>
  );
  
  return (
    <div>
      <h1>{user.name}</h1>
      <button 
        onClick={handleUpdateName}
        disabled={updateMutation.isPending}
      >
        {updateMutation.isPending ? 'Updating...' : 'Update Name'}
      </button>
    </div>
  );
}

// Header 组件(复用相同的数据)
function Header({ userId }) {
  const { data: user } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });
  
  return <div>Welcome, {user?.name}</div>;
}

// Profile 和 Header 同时渲染:
// ✅ 只发一次请求(自动去重)
// ✅ Profile 更新后,Header 自动同步
// ✅ 数据缓存,不会重复请求

对比总结

功能useState 方案React Query 方案
代码量~40 行~30 行
loading/error手动管理自动处理 ✅
缓存自动缓存 ✅
去重请求否(发多次)自动去重 ✅
数据同步手动(很难)自动同步 ✅
错误重试自动重试 ✅
乐观更新需要手写内置支持 ✅
缓存失效无概念自动管理 ✅

核心区别是什么呢?我的理解是:

  • useState:把服务端数据当"普通状态",所有问题都要自己处理
  • React Query:把服务端数据当"远程缓存",这些问题工具已经解决了

这让我开始思考:服务端数据和客户端状态到底有什么本质区别?

理解核心概念:什么是服务端状态?

客户端状态 vs 服务端状态

我觉得理解这个区别很重要。先看客户端状态:

// 客户端状态:数据的"真相源"在客户端

// Modal 开关
const [isOpen, setIsOpen] = useState(false);
setIsOpen(true);  // 立即生效,没有"失败"的可能

// 主题设置
const [theme, setTheme] = useState('dark');
setTheme('light');  // 改了就是改了

// 表单输入
const [name, setName] = useState('');
setName('new name');  // 完全在客户端控制

这些状态的特点是:你说是什么就是什么。没有网络请求,没有失败的可能,刷新页面就丢失。

但服务端状态完全不同:

// 服务端状态:数据的"真相源"在服务器

// 用户信息
const [user, setUser] = useState(null);
fetch('/api/user').then(setUser);

// 问题:
// - 可能请求失败(需要重试)
// - 数据可能过期(服务器上改了)
// - 多处使用时(需要缓存)
// - 刷新页面就丢了(需要重新获取)

关键区别在哪?我整理了这个对比:

维度客户端状态服务端状态
真相源客户端服务器
持久性刷新即丢失服务器持久化
同步问题无(单一真相)有(客户端是副本)
失效判断不需要需要(数据会过期)
网络请求有(可能失败)
缓存需求有(避免重复请求)
适用工具useState/ZustandReact Query/SWR

一个生动的比喻

我想到了一个比喻帮助理解:

客户端状态 = 你的笔记本

  • 你写什么就是什么
  • 想改就改,没有"失败"
  • 丢了就丢了(刷新页面)

服务端状态 = 图书馆的书

  • 你只是借阅,真正的书在图书馆
  • 借之前要去图书馆拿(fetch)
  • 可能借不到(404)
  • 别人可能改了内容(需要同步)
  • 需要判断是否过期(stale)
  • 同一本书可以借给多人(缓存共享)

用 useState 管理"图书馆的书",就像你自己抄了一份。但书的内容变了,你的副本怎么办?这就是为什么需要 React Query。

服务端状态的特殊生命周期

服务端状态有个复杂的生命周期,我试着用流程图表示:

请求数据
   ↓
Loading (加载中)
   ↓
 成功? ←────────┐
   ↓           │
  是   否       │
   ↓    ↓      │
Fresh Error    │
(新鲜) (失败)   │
   ↓    ↓      │
使用  重试? ────┘
数据   ↓
   ↓   否
 过期?  ↓
   ↓  显示错误
  是
   ↓
Stale (过期)
   ↓
后台重新获取
   ↓
Fresh (新鲜)

这个流程里有很多 useState 无法处理的问题:

  • ❌ 失败重试(Error → 重试)
  • ❌ 数据过期判断(Fresh → Stale)
  • ❌ 后台自动刷新(Stale → 重新获取)
  • ❌ 缓存共享(多个组件用同一份数据)

React Query 核心原理

理解了问题,我们再看 React Query 是如何解决的。

Query Key:缓存的唯一标识

React Query 用 Query Key 来标识不同的缓存:

// 环境:React + React Query
// 场景:Query Key 的使用

// 不同的 key = 不同的缓存
useQuery({
  queryKey: ['user'123],  // 用户 123 的数据
  queryFn: () => fetchUser(123),
});

useQuery({
  queryKey: ['user'456],  // 用户 456 的数据
  queryFn: () => fetchUser(456),
});

// 相同的 key = 共享缓存
// 组件 A
useQuery({
  queryKey: ['user'123],
  queryFn: () => fetchUser(123),
});

// 组件 B(使用相同的缓存,不会重复请求)
useQuery({
  queryKey: ['user'123],
  queryFn: () => fetchUser(123),
});

Query Key 的设计很重要。我总结了一些原则:

// ❌ 不好:key 太简单
useQuery({
  queryKey: ['user'],  // 所有用户共享一个缓存?
  queryFn: () => fetchUser(userId),
});

// ✅ 好:key 包含所有影响数据的参数
useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
});

// ✅ 更好:包含查询条件
useQuery({
  queryKey: ['users', { status: 'active', page: 1 }],
  queryFn: () => fetchUsers({ status: 'active', page: 1 }),
});

// 规则:key 变化 = 重新请求

缓存的生命周期

React Query 的缓存有几个状态:

// 环境:React + React Query
// 场景:缓存生命周期配置

const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: fetchUser,
  
  // 缓存配置
  staleTime: 5 * 60 * 1000// 5分钟内认为数据"新鲜"
  gcTime: 10 * 60 * 1000// 缓存保留 10 分钟
});

// 数据状态流转:
// 1. fresh(新鲜):刚获取,不会重新请求
// 2. stale(过期):超过 staleTime,可能会重新请求
// 3. inactive(无人使用):所有组件都卸载了
// 4. deleted(删除):超过 gcTime,从内存中删除

我举个实际例子帮助理解:

// t=0s: 用户打开 Profile 页
useQuery(['user'123], fetchUser);
// → 发起请求
// → 数据返回,状态:fresh

// t=10s: 用户跳到其他页,又跳回 Profile
useQuery(['user'123], fetchUser);
// → 缓存是 fresh(未超过 5 分钟)
// → 直接用缓存,不发请求 ✅

// t=6min: 用户再次打开 Profile
useQuery(['user'123], fetchUser);
// → 缓存是 stale(超过 5 分钟)
// → 先显示缓存数据(不用等待)
// → 后台重新请求更新 ✅

// t=15min: 缓存已删除
useQuery(['user'123], fetchUser);
// → 重新请求

这种机制让用户体验特别好:大部分时候直接显示缓存,不用等待,同时又能保证数据最新。

自动的后台刷新

React Query 还支持自动刷新策略:

// 环境:React + React Query
// 场景:配置自动刷新

const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: fetchUser,
  
  // 自动刷新策略
  refetchOnWindowFocus: true// 窗口获得焦点时刷新
  refetchOnReconnect: true// 网络恢复时刷新
  refetchInterval: 30000// 每 30 秒刷新一次
});

// 用户体验:
// 1. 用户切换到别的标签页
// 2. 5分钟后切回来
// 3. React Query 自动刷新数据
// 4. 用户看到的永远是最新数据 ✅

Mutation:修改数据

查询数据之外,我们还需要修改数据。React Query 用 useMutation 处理:

// 环境:React + React Query
// 场景:修改数据并同步缓存

import { useMutation, useQueryClient } from '@tanstack/react-query';

function EditProfile({ userId }) {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: (newData) => updateUser(userId, newData),
    
    // 成功后的操作
    onSuccess: (updatedUser) => {
      // 方案 1:直接更新缓存(不需要重新请求)
      queryClient.setQueryData(['user', userId], updatedUser);
      
      // 方案 2:让缓存失效,触发重新获取
      queryClient.invalidateQueries({ queryKey: ['user', userId] });
      
      // 方案 3:如果有列表页,也让列表失效
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
  
  return (
    <button 
      onClick={() => mutation.mutate({ name: '新名字' })}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? '保存中...' : '保存'}
    </button>
  );
}

乐观更新(Optimistic Updates)

乐观更新是我觉得特别酷的功能。用户点击按钮,UI 立即变化,不用等待请求完成:

// 环境:React + React Query
// 场景:点赞功能的乐观更新

const likeMutation = useMutation({
  mutationFn: likePost,
  
  // 请求发出前:立即更新 UI
  onMutate: async (postId) => {
    await queryClient.cancelQueries({ queryKey: ['post', postId] });
    
    const previousPost = queryClient.getQueryData(['post', postId]);
    
    // 乐观更新:立即显示"已点赞"
    queryClient.setQueryData(['post', postId], (old) => ({
      ...old,
      liked: true,
      likeCount: old.likeCount + 1,
    }));
    
    return { previousPost };
  },
  
  // 请求失败:回滚
  onError: (err, postId, context) => {
    queryClient.setQueryData(['post', postId], context.previousPost);
    alert('点赞失败');
  },
  
  // 请求成功:确保数据最新
  onSettled: (data, error, postId) => {
    queryClient.invalidateQueries({ queryKey: ['post', postId] });
  },
});

// 用户体验:
// 1. 点击点赞 → UI 立即变化(乐观更新) ✅
// 2. 后台发送请求
// 3a. 成功 → 保持已点赞状态
// 3b. 失败 → 回滚到未点赞,提示错误

对比 useState 的做法:

// useState:必须等请求成功才更新
const [liked, setLiked] = useState(false);

const handleLike = async () => {
  try {
    await likePost(postId);
    setLiked(true);  // 等请求成功才变化
  } catch (err) {
    alert('失败');
  }
};

// 问题:用户点击 → 等待 → 才看到效果(体验差)

请求去重

React Query 还会自动去重请求:

// 环境:React + React Query
// 场景:多个组件同时需要相同数据

function Header() {
  const { data: user } = useQuery({
    queryKey: ['user'123],
    queryFn: () => fetchUser(123),
  });
  return <div>{user?.name}</div>;
}

function Sidebar() {
  const { data: user } = useQuery({
    queryKey: ['user'123],
    queryFn: () => fetchUser(123),
  });
  return <div>{user?.avatar}</div>;
}

// React Query 的行为:
// 1. Header 组件渲染 → 触发请求
// 2. Sidebar 组件渲染(几乎同时)
// 3. React Query 检测到:同样的 queryKey
// 4. 复用第一个请求的结果
// 5. 只发一次网络请求 ✅

// useState 的行为:
// 1. Header 和 Sidebar 各自独立
// 2. 发两次请求 ❌

SWR:更轻量的选择

说到服务端状态管理,还有一个不得不提的工具:SWR。

SWR 的核心理念

SWR 的名字来自 HTTP 缓存策略:Stale-While-Revalidate(过期时重新验证)。它的 API 非常简洁:

// 环境:React + SWR
// 场景:最简洁的服务端状态管理
// 依赖:npm install swr

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then(r => r.json());

function UserProfile({ userId }) {
  const { data, error, isLoading, mutate } = useSWR(
    `/api/users/${userId}`// key(同时也是 URL)
    fetcher
  );
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error</div>;
  
  return <div>{data.name}</div>;
}

注意到了吗?SWR 的用法更简单,key 直接就是 URL。

与 React Query 对比

// React Query:更明确的配置
const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  staleTime: 60000,
});

// SWR:更简洁,约定优于配置
const { data } = useSWR(`/api/users/${userId}`, fetcher);

SWR 的主要特性:

// 环境:React + SWR
// 场景:自动重新验证配置

// 自动重新验证
const { data } = useSWR('/api/user', fetcher, {
  refreshInterval: 3000// 每 3 秒刷新
  revalidateOnFocus: true// 窗口焦点时刷新
  revalidateOnReconnect: true// 网络恢复时刷新
});

// Mutation(更新数据)
import { mutate } from 'swr';

const updateUser = async (newData) => {
  // 乐观更新
  mutate('/api/user', newData, false);
  
  // 发送请求
  await fetch('/api/user', {
    method: 'PUT',
    body: JSON.stringify(newData),
  });
  
  // 重新验证
  mutate('/api/user');
};

选择建议

我整理了这个对比表:

特性React QuerySWR
API 复杂度稍复杂非常简洁 ✅
功能完整度更强大 ✅够用
文件大小~40KB~12KB ✅
DevTools强大 ✅基础
无限滚动内置 ✅需手动实现
学习曲线平缓 ✅
适用场景复杂应用简单应用 ✅

我的建议是:

选 SWR 的情况:

  • ✅ 项目简单,主要是 GET 请求
  • ✅ 追求代码简洁
  • ✅ 使用 Next.js(同一作者 Vercel)

选 React Query 的情况:

  • ✅ 复杂的数据交互
  • ✅ 需要强大的 DevTools
  • ✅ 需要高级功能(无限滚动、依赖查询)

如果让 AI 帮你写代码,SWR 的简单 API 可能更容易生成正确的代码。但如果你的应用比较复杂,React Query 的灵活性会更有价值。

实战场景

理论说了这么多,我们看几个实际场景。

场景 1:用户中心(数据共享)

问题:用户信息在多处使用,如何避免重复请求?

// 环境:React + React Query
// 场景:多个组件共享用户数据

// Header
function Header() {
  const { data: user } = useQuery({
    queryKey: ['currentUser'],
    queryFn: fetchCurrentUser,
  });
  return <div>Hi, {user?.name}</div>;
}

// Sidebar
function Sidebar() {
  const { data: user } = useQuery({
    queryKey: ['currentUser'],
    queryFn: fetchCurrentUser,
  });
  return <UserAvatar src={user?.avatar} />;
}

// Profile
function Profile() {
  const { data: user } = useQuery({
    queryKey: ['currentUser'],
    queryFn: fetchCurrentUser,
  });
  return <UserDetails user={user} />;
}

// 结果:三个组件,只发一次请求 ✅
// useState 做不到这一点

场景 2:列表 + 详情(数据同步)

问题:详情页修改后,列表页如何同步?

// 环境:React + React Query
// 场景:列表和详情的数据同步

// 列表页
function PostList() {
  const { data: posts } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  });
  
  return posts?.map(post => <PostCard key={post.id} post={post} />);
}

// 详情页
function PostDetail({ postId }) {
  const queryClient = useQueryClient();
  
  const updateMutation = useMutation({
    mutationFn: updatePost,
    onSuccess: () => {
      // 让列表页缓存失效
      queryClient.invalidateQueries({ queryKey: ['posts'] });
      // 列表页自动刷新 ✅
    },
  });
  
  return <button onClick={() => updateMutation.mutate(newData)}>Update</button>;
}

场景 3:搜索(防抖 + 缓存)

问题:搜索框实时搜索,如何优化?

// 环境:React + React Query
// 场景:搜索功能的优化
// 依赖:npm install use-debounce

import { useDebounce } from 'use-debounce';

function SearchBox() {
  const [keyword, setKeyword] = useState('');
  const [debouncedKeyword] = useDebounce(keyword, 500);
  
  const { data: results } = useQuery({
    queryKey: ['search', debouncedKeyword],
    queryFn: () => searchAPI(debouncedKeyword),
    enabled: debouncedKeyword.length > 0,
    staleTime: 5 * 60 * 1000// 缓存 5 分钟
  });
  
  return (
    <div>
      <input
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)}
      />
      {results?.map(item => <SearchResult key={item.id} item={item} />)}
    </div>
  );
}

// 优势:
// - 输入 "react" → 等 500ms → 搜索
// - 再次搜索 "react" → 直接用缓存 ✅
// - 不需要手动管理缓存逻辑

场景 4:无限滚动

React Query 内置了无限滚动的支持:

// 环境:React + React Query
// 场景:无限滚动列表

import { useInfiniteQuery } from '@tanstack/react-query';

function InfinitePostList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
    getNextPageParam: (lastPage) => lastPage.nextPage,
    initialPageParam: 1,
  });
  
  return (
    <div>
      {data?.pages.map(page =>
        page.posts.map(post => <PostCard key={post.id} post={post} />)
      )}
      {hasNextPage && (
        <button 
          onClick={() => fetchNextPage()}
          disabled={isFetchingNextPage}
        >
          {isFetchingNextPage ? 'Loading...' : '加载更多'}
        </button>
      )}
    </div>
  );
}

最佳实践与常见陷阱

在使用 React Query 的过程中,我总结了一些经验。

Query Key 设计原则

// ❌ 不好:key 不够具体
useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
});

// ✅ 好:包含所有影响数据的参数
useQuery({
  queryKey: ['user', userId, { includeProfile: true }],
  queryFn: () => fetchUser(userId, { includeProfile: true }),
});

避免过度失效缓存

// ❌ 不好:让所有缓存失效
queryClient.invalidateQueries();

// ✅ 好:精确失效
queryClient.invalidateQueries({ queryKey: ['user', userId] });

// ✅ 更好:直接更新缓存(避免重新请求)
queryClient.setQueryData(['user', userId], newData);

常见陷阱

陷阱 1:忘记处理 undefined

// ❌ 错误
const { data } = useQuery({ queryKey: ['user'], queryFn: fetchUser });
return <div>{data.name}</div>;  // 报错!data 初始是 undefined

// ✅ 正确
return <div>{data?.name}</div>;

陷阱 2:缓存时间设置不当

// ❌ 不好:股票价格缓存 1 小时
const { data } = useQuery({
  queryKey: ['stock'],
  queryFn: fetchStock,
  staleTime: 60 * 60 * 1000// 数据会过期的!
});

// ✅ 好:根据数据特性设置
const { data } = useQuery({
  queryKey: ['stock'],
  queryFn: fetchStock,
  staleTime: 1000// 1秒后过期
  refetchInterval: 5000// 每5秒刷新
});

陷阱 3:在循环中使用 useQuery

// ❌ 错误:Hooks 不能在循环中
userIds.map(id => {
  const { data } = useQuery({ queryKey: ['user', id], ... });
});

// ✅ 正确:使用 useQueries
const results = useQueries({
  queries: userIds.map(id => ({
    queryKey: ['user', id],
    queryFn: () => fetchUser(id),
  })),
});

延伸与发散

写到这里,我又产生了一些新的思考:

与 Zustand 的配合

React Query 和 Zustand 各司其职:

// React Query:管理服务端数据
const { data: user } = useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
});

// Zustand:管理客户端状态
const useUIStore = create((set) => ({
  sidebarOpen: false,
  toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
}));

// 各司其职,不要混用

SSR 中的使用

在 Next.js 等 SSR 框架中,React Query 也能很好地工作:

// 环境:Next.js + React Query
// 场景:服务端预取数据

export async function getServerSideProps({ params }) {
  const queryClient = new QueryClient();
  
  // 服务端预取数据
  await queryClient.prefetchQuery({
    queryKey: ['user', params.id],
    queryFn: () => fetchUser(params.id),
  });
  
  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  };
}

// 客户端直接使用服务端数据
function UserPage({ id }) {
  const { data: user } = useQuery({
    queryKey: ['user', id],
    queryFn: () => fetchUser(id),
  });
  
  return <div>{user.name}</div>;  // 首屏直接显示,无 loading
}

未来的思考

  • React Server Components 会如何改变服务端状态管理?
  • AI 流式输出适合用 React Query 吗?
  • 大量缓存会导致内存问题吗?
  • Suspense 模式的优缺点是什么?

这些问题我还在探索中,如果你有想法,欢迎交流。

小结

这篇文章从一个完整的对比开始,试图帮助你理解"为什么需要专门工具管理服务端数据"。

我的核心收获是:

  • 服务端数据 ≠ 普通状态——它本质上是远程缓存
  • useState 无法处理的问题:缓存、同步、失效、重试
  • React Query/SWR 已经解决了这些问题,不需要重复造轮子

实用建议:

  • 服务端数据 → React Query/SWR
  • 客户端状态 → useState/Zustand
  • 不要混用,分工明确

但我也想保持开放的态度。这些工具是否是"最佳实践"?在不同的项目规模、团队背景下,答案可能不同。重要的是理解背后的原理,而不是盲目跟风。

你之前是怎么管理服务端数据的?踩过哪些坑?准备尝试 React Query 吗?

参考资料