反模式: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 |
→ 下一篇 路由方案对比