反模式:useEffect + fetch

const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
  fetch(url)
    .then(r => r.json())
    .then(setData)
    .catch(setError)
    .finally(() => setLoading(false));
}, [url]);

为什么不行

  • 没缓存(路由切回又拉)
  • 没去重(两个组件同时拉一样的)
  • 没重试
  • 没竞态保护
  • 没 focus refetch
  • SSR 不友好

学习用可以,生产别这样

现代客户端方案:TanStack Query

npm i @tanstack/react-query
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const qc = new QueryClient({
  defaultOptions: {
    queries: { staleTime: 60_000, refetchOnWindowFocus: false },
  },
});

export function Providers({ children }) {
  return <QueryClientProvider client={qc}>{children}</QueryClientProvider>;
}
function User({ id }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', id],
    queryFn: () => fetch(`/api/user/${id}`).then(r => r.json()),
  });
  if (isLoading) return <p>加载中</p>;
  if (error) return <p>失败</p>;
  return <div>{data.name}</div>;
}

变更:

const qc = useQueryClient();
const mutation = useMutation({
  mutationFn: (data) => fetch('/api/save', { method: 'POST', body: JSON.stringify(data) }),
  onSuccess: () => qc.invalidateQueries({ queryKey: ['user'] }),
});
mutation.mutate({ name: 'x' });

SWR

SWR 是 Vercel 出的同类,API 更轻:

import useSWR from 'swr';

const fetcher = (url: string) => fetch(url).then(r => r.json());
const { data, error, isLoading } = useSWR(`/api/user/${id}`, fetcher);

特性比 TanStack Query 少(变更、并发、infinite 都更弱),但是学习曲线更平缓。简单场景两者都行。

RSC:根本不需要客户端库

// app/users/[id]/page.tsx (Server Component)
// Next.js 15+:params 是 Promise,必须 await
export default async function UserPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const user = await db.user.findUnique({ where: { id } });
  return <UserCard user={user} />;
}

数据在服务器拉、HTML 流到浏览器、Bundle 里没有数据获取代码。这是 2024+ Next.js 推荐做法。

变更走 Server Actions(react-quick 14 篇):

'use server';
export async function updateUser(formData: FormData) {
  await db.user.update({...});
  revalidatePath('/users');
}

何时还需要客户端获取

RSC 不能取代客户端获取的场合:

  • 依赖客户端状态(如根据 useState 变化拉数据)
  • 频繁实时更新(轮询、WebSocket 同步显示)
  • 乐观更新需要复杂回滚
  • 分页 / 无限滚动 + 客户端缓存共享

这时候在 Client Component 用 TanStack Query。

混合策略(最常见)

// Server Component:拿初始数据
async function Page() {
  const initialPosts = await db.post.findMany({ take: 20 });
  return <PostList initialData={initialPosts} />;
}

// Client Component:后续翻页、刷新、变更
'use client';
function PostList({ initialData }) {
  const { data } = useInfiniteQuery({
    queryKey: ['posts'],
    initialData: { pages: [initialPosts], pageParams: [0] },
    queryFn: ({ pageParam = 0 }) => fetch(`/api/posts?cursor=${pageParam}`).then(r => r.json()),
    ...
  });
  return ...;
}

首屏快(SSR 出数据),后续交互流畅(客户端缓存)。

缓存层心智

浏览器内存(TanStack Query 缓存)
  ↓ miss
HTTP cache (Cache-Control 头)
  ↓ miss
Next.js Data Cache(fetch 自动缓存)
  ↓ miss
后端缓存(Redis)
  ↓ miss
数据库

每一层都可能命中。写代码时要清楚自己在哪一层——不然性能问题没法定位。

工具速查

工具 类型 优势
TanStack Query 客户端 SQL/REST 功能最全、最主流
SWR 客户端 SQL/REST 轻量、Vercel 出品
Apollo Client 客户端 GraphQL GraphQL 生态首选
urql 客户端 GraphQL 比 Apollo 轻
RTK Query 客户端 SQL/REST Redux 用户用
RSC + Server Actions 服务端 Next.js 首选
tRPC 全栈类型安全 自家前后端、不需要 OpenAPI

→ 下一篇 路由方案对比