楼层: 首页/ 软件技术/ 前端进阶/ 第 1 章 React 生态:路由、状态、数据、UI
链

第 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
UIshadcn/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)都帮你把这套接住。语法几乎一样,差别在功能体量。

维度SWRTanStack Query
定位轻量,<5KB,"够用就好"全家桶,功能全(分页、无限滚动、缓存持久化、devtools)
核心 HookuseSWR(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 设计高度相似,学会一个另一个半天就能切换。