链
第 1 章 React 生态:路由、状态、数据、UI
React Router / Zustand / TanStack Query / shadcn/ui
React 本身只管"画组件",真实项目还需要一堆周边:路由(URL 变了换页面)、状态管理(跨组件共享数据)、数据请求(带缓存的 fetch)、UI 组件库(别自己造轮子)。这一章把当前主流选择一次讲清。
路由 React Router 7:URL 变了不刷新页面
SPA 单页应用里,用户点链接不应该整页刷新(那样体验像 2005 年)。React Router 监听 URL 变化,只换对应的组件。
import { createBrowserRouter, RouterProvider, Link, NavLink } from "react-router";
const router = createBrowserRouter([
{ path: "/", element: <Home /> },
{ path: "/about", element: <About /> },
{ path: "/users/:id", element: <UserDetail /> }, // 动态参数
{ path: "*", element: <NotFound /> }, // 404
]);
export default function App() {
return <RouterProvider router={router} />;
}
在组件里拿参数、跳转
import { useParams, useNavigate } from "react-router";
function UserDetail() {
const { id } = useParams(); // 拿 URL 里的 :id
const navigate = useNavigate(); // 编程式跳转
return <button onClick={() => navigate("/")}>回首页</button>;
}
状态管理:从 Context 到 Zustand
论什么时候才需要状态管理库
组件间传数据靠 props,但层层往下传(props drilling)太痛苦。小场景用 React 内置的 Context 就够;中大型项目上 Zustand;超大型团队协作上 Redux Toolkit。
Zustand(读"佐伊斯坦德")是当前最流行的轻量方案:一个 hook 全搞定,API 极简。
// stores/useAuth.ts —— Zustand 全局状态
import { create } from "zustand";
type AuthState = {
user: { name: string } | null;
login: (name: string) => void;
logout: () => void;
};
export const useAuth = create<AuthState>((set) => ({
user: null,
login: (name) => set({ user: { name } }),
logout: () => set({ user: null }),
}));
// 任何组件里直接用,不用 Provider
function Header() {
const { user, logout } = useAuth();
return <header>{user?.name ?? "未登录"} <button onClick={logout}>退出</button></header>;
}
数据请求:TanStack Query
自己写 fetch + useEffect 会发现一堆麻烦:缓存、重试、loading、过期刷新……TanStack Query(原名 React Query)把这些全搞定,是数据请求的事实标准。
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
// 查数据:自动缓存、后台刷新、重试
function UserProfile({ id }: { id: number }) {
const { data, isLoading, error } = useQuery({
queryKey: ["user", id], // 缓存的 key,id 变了重新拉
queryFn: () => fetch(`/api/users/${id}`).then(r => r.json()),
});
if (isLoading) return <p>加载中</p>;
if (error) return <p>出错了</p>;
return <h1>{data.name}</h1>;
}
// 改数据:mutation 自动失效缓存
function EditProfile() {
const qc = useQueryClient();
const mutation = useMutation({
mutationFn: (newName: string) =>
fetch("/api/users/me", { method: "PATCH", body: JSON.stringify({ name: newName }) }),
onSuccess: () => qc.invalidateQueries({ queryKey: ["user"] }), // 自动重拉
});
/* ... */
}
UI 组件库选型
| 方案 | 适合谁 |
| Ant Design 5/6 | 企业级中后台,组件全,文档中文友好。后台管理系统首选。 |
| shadcn/ui | 当前最火:组件代码直接复制到你项目里,完全可控可改。配合 Tailwind。 |
| Material UI (MUI) | Google Material Design 风格,海外产品用得多。 |
| Radix UI / Headless UI | 无样式、无障碍好,自己套 Tailwind 皮。 |
| Vant | 移动端 H5 组件库。 |
表单与样式方案
表单用 React Hook Form(性能好、非受控)+ zod(Schema 校验,跟 TS 打通)。样式当前主流是 Tailwind CSS——utility-first,不写 CSS 类名,直接在 JSX 里写 class="flex gap-2 p-4"。
// Tailwind 长这样:不用建 .css 文件
<button class="px-4 py-2 bg-pine text-white rounded-lg hover:opacity-90">
提交
</button>
// react-hook-form + zod 一行校验
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.object({
email: z.string().email("邮箱格式不对"),
password: z.string().min(6, "至少 6 位"),
});
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
});
/* ... */
}
状态管理四大门派:Redux Toolkit / Zustand / Jotai / MobX
选一句话选型
Zustand:中小项目首选,几行代码一个 store,心智负担最低。Redux Toolkit:大型团队、需要时间旅行调试和严格规范时用。Jotai:原子化状态,组件级细粒度更新,适合状态碎片多的应用。MobX:响应式风格,写起来像 Vue,喜欢自动追踪依赖的人爱它。
① Redux Toolkit 完整实战:计数器 + 异步数据
// store/index.ts —— configureStore 组装
import { configureStore, createSlice, createAsyncThunk, createSelector } from "@reduxjs/toolkit";
import { Provider, useSelector, useDispatch } from "react-redux";
// 异步 thunk:封装 fetch
export const fetchUser = createAsyncThunk("user/fetch", async (id: number) => {
const r = await fetch(`/api/users/${id}`);
return r.json();
});
// slice:把 state + reducer + action 打包
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
inc: (state) => { state.value += 1; }, // RTK 用 Immer,可以"直接改"
add: (state, action) => { state.value += action.payload; },
},
extraReducers: (builder) => {
builder.addCase(fetchUser.fulfilled, (state, action) => {
state.lastId = action.payload.id;
});
},
});
export const { inc, add } = counterSlice.actions;
export const store = configureStore({ reducer: { counter: counterSlice.reducer } });
// App 顶层包 <Provider store={store}> ...
// 组件里:useSelector 取,useDispatch 派
function Counter() {
const value = useSelector(s => s.counter.value);
const dispatch = useDispatch();
return <button onClick={() => dispatch(inc())}>{value}</button>;
}
② Zustand 完整实战:Todo + persist 持久化 + devtools
import { create } from "zustand";
import { persist, devtools } from "zustand/middleware";
type Todo = { id: number; text: string; done: boolean };
type TodoState = {
todos: Todo[];
add: (text: string) => void;
toggle: (id: number) => void;
remove: (id: number) => void;
};
export const useTodo = create<TodoState>()(
persist(
devtools((set) => ({
todos: [],
add: (text) => set((s) => ({ todos: [...s.todos, { id: Date.now(), text, done: false }] })),
toggle: (id) => set((s) => ({ todos: s.todos.map(t => t.id === id ? { ...t, done: !t.done } : t) })),
remove: (id) => set((s) => ({ todos: s.todos.filter(t => t.id !== id) })),
})),
{ name: "todo-store" } // persist 自动存 localStorage
)
);
// 选择器只订阅需要的字段:todos 不变就不重渲染
function TodoCount() {
const count = useTodo(s => s.todos.length);
return <p>共 {count} 条</p>;
}
③ Jotai:原子化,每个 atom 是一个独立的"小 store"
import { atom, useAtom, useAtomValue } from "jotai";
const countAtom = atom(0);
const doubleAtom = atom((get) => get(countAtom) * 2); // 派生 atom
// 异步 atom:直接 await fetch
const userAtom = atom(async (get) => {
const r = await fetch("/api/me");
return r.json();
});
function Counter() {
const [count, setCount] = useAtom(countAtom);
const doubled = useAtomValue(doubleAtom); // 只读
return <button onClick={() => setCount(c => c + 1)}>{count} / {doubled}</button>;
}
④ MobX:响应式风格,自动追踪依赖
import { makeAutoObservable, runInAction } from "mobx";
import { observer } from "mobx-react-lite";
class TodoStore {
todos = [];
constructor() { makeAutoObservable(this); }
get remaining() { // computed,自动缓存
return this.todos.filter(t => !t.done).length;
}
add(text) { // action,改 state
this.todos.push({ id: Date.now(), text, done: false });
}
}
const store = new TodoStore();
const TodoList = observer(function TodoList() {
// observer 包裹后,store.remaining 变了组件自动重渲染
return <p>剩余 {store.remaining} 条</p>;
});
| 方案 | 学习成本 | 样板代码 | 性能 | 适合场景 |
| Zustand | 极低 | 极少 | 好(选择器) | 90% 中小项目的默认答案。 |
| Redux Toolkit | 中 | 中 | 好 | 大型团队、需要 devtools 时间旅行和严格规约。 |
| Jotai | 低 | 少 | 极好(原子级) | 状态碎片多、追求最小重渲染。 |
| MobX | 中 | 少 | 极好(响应式) | 从 Vue 转来、喜欢自动追踪依赖。 |
数据获取:TanStack Query 完整 CRUD + 乐观更新
TanStack Query 管的是"服务端状态"(从后端拿回来的数据),Zustand/Redux 管的是"客户端状态"(登录态、主题、UI 开关)。别用 Zustand 缓存列表数据,那是拿错了工具。
QueryClient 配置 + 完整用户 CRUD
// main.tsx 顶层
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 60_000, retry: 1, refetchOnWindowFocus: false } },
});
<QueryClientProvider client={queryClient}><App /></QueryClientProvider>
// 用户列表
function UserList() {
const { data } = useQuery({
queryKey: ["users"],
queryFn: () => fetch("/api/users").then(r => r.json()),
});
// 创建/删除/编辑 mutation
const qc = useQueryClient();
const del = useMutation({
mutationFn: (id) => fetch(`/api/users/${id}`, { method: "DELETE" }),
onSettled: () => qc.invalidateQueries({ queryKey: ["users"] }), // 成功失败都重拉
});
// ...
}
// 乐观更新:先改 UI,失败再回滚
const patch = useMutation({
mutationFn: (u) => fetch(`/api/users/${u.id}`, { method: "PATCH", body: JSON.stringify(u) }),
onMutate: async (u) => {
await qc.cancelQueries({ queryKey: ["users"] });
const prev = qc.getQueryData(["users"]);
qc.setQueryData(["users"], (old) => old.map(x => x.id === u.id ? { ...x, ...u } : x));
return { prev }; // context,给 onError 回滚用
},
onError: (e, u, ctx) => qc.setQueryData(["users"], ctx.prev),
onSettled: () => qc.invalidateQueries({ queryKey: ["users"] }),
});
SWR(useSWR/mutate)是同思路的轻量版,API 更简;TanStack Query 功能更全(分页、无限滚动、Devtools、mutation 生命周期)。新项目默认选 TanStack Query。
表单:React Hook Form 完整实战
传统受控组件每敲一个字都 setState 一次,大表单卡得要死。React Hook Form 用非受控 + ref,只在提交时校验,性能好一个量级。配 zod 做 Schema 校验。
注册表单:register + zodResolver + useFieldArray
import { useForm, useFieldArray, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.object({
username: z.string().min(2, "至少 2 字"),
email: z.string().email("邮箱不对"),
password: z.string().min(6, "至少 6 位"),
tags: z.array(z.object({ name: z.string() })), // 动态字段
});
type FormIn = z.infer<typeof schema>;
function RegForm() {
const { register, control, handleSubmit, formState: { errors, isSubmitting } } =
useForm<FormIn>({ resolver: zodResolver(schema), defaultValues: { tags: [{ name: "" }] } });
const { fields, append, remove } = useFieldArray({ control, name: "tags" });
const onSubmit = async (v: FormIn) => { /* POST /api/register */ };
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input placeholder="用户名" {...register("username")} />
{errors.username && <p>{errors.username.message}</p>}
<input placeholder="邮箱" {...register("email")} />
<input type="password" {...register("password")} />
{fields.map((f, i) => (
<div key={f.id}>
<input {...register(`tags.${i}.name`)} />
<button type="button" onClick={() => remove(i)}>删</button>
</div>
))}
<button type="button" onClick={() => append({ name: "" })}>加标签</button>
<button disabled={isSubmitting}>注册</button>
</form>
);
}
Formik(useFormik/Form/Field + yup)是上一代方案,API 更啰嗦、性能不如 RHF,新项目默认 RHF。
UI 层:shadcn/ui + Tailwind CSS
皮shadcn/ui 不是 npm 包
shadcn/ui 的思路与众不同:不是 npm install shadcn,而是用 CLI 把组件源码复制到你项目里(基于 Radix UI 无头组件 + Tailwind CSS 样式),想怎么改就怎么改。它火就火在"你拥有全部代码"。
pnpm dlx shadcn@latest init # 初始化:选风格、配色
pnpm dlx shadcn@latest add button dialog table form # 把组件源码加进 src/ui/
// 之后直接用,完全可控
<Button variant="outline">点我</Button>
<Dialog>...</Dialog>
Tailwind CSS:utility-first,一行类名搞定样式
/* Tailwind 3:tailwind.config.js 配主题;Tailwind 4:CSS 里一行 @import 即可 */
<div class="max-w-md mx-auto p-6 rounded-xl bg-white shadow-lg
hover:shadow-xl transition
dark:bg-neutral-900 sm:p-8 md:p-10">
<h2 class="text-xl font-bold text-gray-900">响应式卡片</h2>
<p class="mt-2 text-sm text-gray-500">sm: md: lg: 前缀控制断点;
hover: focus: dark: 前缀控制状态;不用写一行 CSS。</p>
</div>
/* 需要复用样式时 @apply 抽成 class */
.card-title { @apply text-xl font-bold text-gray-900; }
Ant Design 5/6:企业级中后台之王,ConfigProvider 一键换主题/切明暗/国际化,表格表单弹窗开箱即用。MUI:Google Material 风格,海外产品用得多,ThemeProvider 定制主题。中后台选 AntD,做自己产品的品牌选 shadcn/ui + Tailwind。
完整案例:React 19 博客管理后台技术栈
| 层 | 选型 |
| 框架 | React 19 + TypeScript + Vite |
| 路由 | React Router 7(数据路由 + 懒加载) |
| 状态 | Zustand(auth + 主题) |
| 数据 | TanStack Query(文章 CRUD、缓存、乐观更新) |
| 表单 | React Hook Form + zod |
| UI | shadcn/ui + Tailwind CSS |
架构分层照后端分包思路:src/api/(接口层)、src/stores/(Zustand)、src/pages/(页面)、src/components/(通用组件)、src/layouts/(后台布局)。main.tsx 顶层依次套 QueryClientProvider → RouterProvider;路由表用 createBrowserRouter,受保护路由外层包一个 AuthGuard,未登录跳 /login。这套骨架就是上面第 6 章参考工程的缩水版,照着搭即可。
本章面试题
Q1. Zustand 和 Redux Toolkit 的核心区别?什么时候必须上 RTK?
看答案
Zustand 一个 create 函数建 store,无样板、无 Provider(可选),选择器精准订阅;RTK 有 action/reducer/dispatch 的规范分层、配套 devtools 时间旅行。团队大、需要严格规约和可追踪性时选 RTK;中小项目 Zustand 写起来快十倍。
Q2. TanStack Query 的 staleTime 和 cacheTime 分别是什么?
看答案
staleTime:数据"新鲜"多久,新鲜期内不重新请求;cacheTime:缓存保留多久,过了就从内存清掉。staleTime 影响"要不要重拉",cacheTime 影响"缓存能活多久"。
Q3. React Hook Form 为什么比受控表单快?
看答案
它用 ref 注册输入框而非受控 value+onChange,打字不触发 React 重渲染,只在 blur/submit 时读值。几百个字段的大表单差距巨大。
Q4. shadcn/ui 为什么"不是 npm 包"?
看答案
它把 Radix UI + Tailwind 组件源码直接拷进你的项目,你完全拥有代码、可以随便改,升级靠 CLI 对比合并,不产生一个黑盒依赖。
数据请求库二选一:TanStack Query vs SWR
自己用 useEffect + fetch 管数据,要手写 loading / error / 缓存 / 重试 / 重新校验,重复造轮子。SWR(Vercel 出品,名字来自 stale-while-revalidate)和 TanStack Query(原 React Query)都帮你把这套接住。语法几乎一样,差别在功能体量。
| 维度 | SWR | TanStack Query |
| 定位 | 轻量,<5KB,"够用就好" | 全家桶,功能全(分页、无限滚动、缓存持久化、devtools) |
| 核心 Hook | useSWR(key, fetcher) | useQuery/useMutation |
| 缓存策略 | stale-while-revalidate:先拿旧缓存显示,后台刷新再替换 | 同款 SWR 策略 + 细粒度 staleTime / gcTime 配置 |
| 适用 | 小项目、个人站、想要极简 | 中大型后台、需要乐观更新/分页/复杂缓存 |
SWR 就一个 Hook,对比上面 TanStack Query 的写法
import useSWR from 'swr';
function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetch);
if (isLoading) return <p>加载中</p>;
if (error) return <p>出错了</p>;
return <h1>{data.name}</h1>;
}
选型一句话:新团队新项目、后台管理系统要 CRUD + 乐观更新,直接上 TanStack Query;追求包体小、交互简单(个人博客、文档站),SWR 更轻。API 设计高度相似,学会一个另一个半天就能切换。