应
第 7 章 React 19:组件化的声明式 UI
React 19 — Components, Hooks, State
React 是 Meta(Facebook)出的前端框架,2013 年发布,至今生态最大。核心思想一句话:UI = f(state)——你只描述"状态是什么样时页面长什么样",React 负责把 DOM 改成那个样子。你不用手动操作 DOM,只要改 state。
念React 三个核心概念
组件化:把页面拆成独立、可复用的零件(按钮、卡片、表单),每个零件自己管自己的样子和逻辑。
声明式:你不说"点按钮后把文字改成 xxx",而是说"当 count 是 3 时,这里显示 3"。React 负责 diff 并更新 DOM。
单向数据流:数据从父组件通过 props 流向子组件,子组件想改数据就调用父组件传下来的回调。不搞双向绑定那一套,bug 少很多。
JSX:在 JS 里写 HTML
// JSX 不是模板字符串,是 React.createElement 的语法糖
function Welcome({ name }: { name: string }) {
const age = 18;
return (
<div className="card">{/* className 代替 class */}
<h1>你好,{name}</h1>
<p>年龄:{age >= 18 ? "成年" : "未成年"}</p>
<button onClick={() => alert("点了")}>点我</button>
</div>
);
}
注意几个细节:class 要写成 className(class 是 JS 关键字);样式用对象 style={{ color: "red" }};列表渲染必须给 key。
State:组件内部的记忆
import { useState } from "react";
function Counter() {
// useState 返回 [当前值, 更新函数]
const [count, setCount] = useState(0);
return (
<div>
<p>当前:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
React 铁律:状态不可变
不要直接改 state。arr.push(x) 不会触发 React 重渲染,因为引用没变。正确做法是返回新数组:setArr([...arr, x]) 或 setArr(arr.map(...))。
setState 是异步批量的。连续两次 setCount(count+1) 只会加一次。要用函数式更新:setCount(prev => prev + 1)。
useEffect:处理副作用
"副作用"就是"组件渲染之外干的事":发请求、订阅、操作 DOM、开定时器。useEffect 是 React 最容易用错的 Hook。
import { useState, useEffect } from "react";
function UserProfile({ id }: { id: number }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
let cancelled = false;
async function load() {
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
if (!cancelled) setUser(data); // 防竞态
}
load();
// 清理函数:组件卸载或下次 effect 前执行
return () => { cancelled = true; };
}, [id]); // 依赖数组:id 变了才重新跑
if (!user) return <p>加载中...</p>;
return <h1>{user.name}</h1>;
}
useEffect 三大坑
① 依赖数组写错:漏写依赖 → 闭包读到旧值;多写依赖 → 无限循环。ESLint 的 exhaustive-deps 规则会帮你检查。
② 直接在 effect 里 setState:会触发重渲染,又触发 effect,又 setState……死循环。
③ 没写清理函数:定时器、事件监听、订阅没清,组件卸载后还在跑 → 内存泄漏。
其他常用 Hook
| Hook | 干嘛的 |
useRef | 存一个不触发渲染的可变值,或拿到 DOM 元素。 |
useMemo | 缓存计算结果,依赖不变不重算。 |
useCallback | 缓存函数引用,避免子组件无谓重渲染。 |
useContext | 跨多层组件共享数据,避免 props 一层一层传。 |
useReducer | 复杂状态逻辑用 reducer 管理,类似 Redux。 |
自定义 Hook:把逻辑抽出来复用
// hooks/useDebounce.ts —— 防抖 Hook
import { useState, useEffect } from "react";
export function useDebounce<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const t = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(t);
}, [value, delay]);
return debounced;
}
// 组件里用:搜索框输入停 300ms 才发请求
function Search() {
const [kw, setKw] = useState("");
const debouncedKw = useDebounce(kw, 300);
useEffect(() => { if (debouncedKw) searchApi(debouncedKw); }, [debouncedKw]);
return <input value={kw} onChange={e => setKw(e.target.value)} />;
}
React 19 新特性速览
- Actions:
useActionState 把异步提交、pending 状态、错误处理一锅端。
- useOptimistic:乐观更新——先假装成功,失败了再回滚。
- Server Components:服务端组件,直接在服务端跑,不发到浏览器,体积小。
- use():在组件里直接读 Promise 或 Context,不用 useEffect。
- 文档元数据:
<title>、<meta> 可以直接写在组件里,React 自动管。
React 19 深化:Hooks 全清单、性能、状态管理与路由
Hooks 就是函数组件的超能力。入门只会 useState/useEffect 不算数,这里把官方 Hooks 按用途摊开,外加 React 19 几个新 Hooks。
JSX 原理与列表渲染
// JSX 不是真 HTML,是 React.createElement 的语法糖
// <div className="a">hi</div> 编译成 React.createElement("div", {className:"a"}, "hi")
// React 19 + modern 配置下,Babel 自动 jsx-runtime,不用手动 import React
// 条件渲染:&& / 三元 / IIFE
{loading && <p>加载中</p>}
{error ? <ErrorBox/> : <Content/>}
// 列表:key 用稳定 id,不要用 index!
{items.map(item => (
<Todo key={item.id} {...item} /> // index 当 key:列表插入/删除时 React 复用错 DOM
))}
// Fragment:不产生多余 DOM 节点
<><dt>词</dt><dd>解释</dd></>
Hooks 全清单(按用途)
| 分类 | Hooks |
| 状态 | useState(含惰性初始化 useState(() => heavy())、函数式更新)、useReducer(复杂状态、动作派发)。 |
| 副作用 | useEffect(渲染后异步跑)、useLayoutEffect(DOM 更新后、绘制前同步跑,测尺寸用)、useInsertionEffect(CSS-in-JS 库注入样式用)。 |
| 引用/优化 | useRef(DOM 句柄 / 不触发渲染的可变值)、useMemo(缓存计算)、useCallback(缓存函数引用)、useImperativeHandle(自定义暴露给父组件的 ref 句柄)。 |
| 共享 | useContext。 |
| React 19 新 | useActionState、useFormStatus、useOptimistic、use()。另有 useId(SSR 唯一 ID,无障碍用)、useSyncExternalStore(订阅外部 store,并发安全)。 |
React 19 新 Hooks 实战:表单提交 + 乐观更新
// useActionState:把异步 action + pending + 结果一锅端
import { useActionState, useFormStatus } from "react";
// action:(上一次状态, 表单数据) => 新状态
async function submitTodo(prev: { ok: boolean } | null, fd: FormData) {
const title = fd.get("title") as string;
await fetch("/api/todos", { method: "POST", body: fd });
return { ok: true };
}
function SubmitButton() {
// useFormStatus 必须用在 <form> 内部的子组件
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "提交中..." : "添加"}</button>;
}
function TodoForm() {
const [state, action] = useActionState(submitTodo, null);
return (
<form action={action}>
<input name="title" />
<SubmitButton /> // 自动拿到 pending,不用 props 传
</form>
);
}
// useOptimistic:先显示"预期结果",失败再回滚
function useOptimisticTodos(todos: Todo[]) {
const [optimistic, addOptimistic] = useOptimistic(todos, (curr, newTodo: Todo) => [
...curr, newTodo,
]);
return { optimistic, addOptimistic };
}
use():在渲染里直接读 Promise(配 Suspense)
function Profile({ p }: { p: Promise<User> }) {
const user = use(p); // 不用 useEffect + useState
return <h1>{user.name}</h1>;
}
// 外层用 Suspense 兜底
<Suspense fallback={<p>加载中...</p>}>
<Profile p={fetchUser(id)} />
</Suspense>
性能优化:什么时候才需要 memo
优React 性能三板斧,按顺序用
① 别过早优化。React 很快,先写对,Profile 测出瓶颈再优化。
② 状态下放:state 放在真正需要它的最低公共父组件,别让一个小状态变化导致整棵树重渲染。
③ React.memo:对 props 浅比较,props 没变就跳过重渲染。但别滥用——浅比较本身也有成本。
④ useMemo/useCallback:把计算结果或函数引用稳住,让 memo 真的生效。注意:别因为"听说要优化"就到处包,多数情况不需要。
⑤ 懒加载:const Page = React.lazy(() => import("./Page")),路由级代码分割。
⑥ 并发渲染:useTransition / useDeferredValue 把"非紧急更新"(输入联想)标记为过渡,让出主线程保持响应。
虚拟 DOM 是什么:就是真实 DOM 的草稿纸。你改 state,React 先在内存里生成一棵新的虚拟 DOM 树,和旧的做 Diff,算出最小改动,再批量打到真实 DOM 上。手动操作 DOM 是"直接改真纸",React 是"先在草稿上演算"。
状态管理选型
| 方案 | 适用 |
| useState / useReducer | 组件本地状态,90% 场景够了。 |
| Context + useReducer | 轻量全局(主题、用户登录态),别塞高频更新状态。 |
| Zustand | 极简全局 store,无 Provider、选择器订阅,中小项目首选。 |
| Redux Toolkit | 大型应用、团队规范、需要 devtools/中间件生态。 |
| Jotai / Recoil | 原子化状态,依赖图自动更新。 |
| TanStack Query / SWR | 服务端状态:缓存、重试、失效、后台刷新——别自己在 useState 里管接口数据。 |
Zustand + TanStack Query 核心代码
// stores/useTaskStore.ts —— Zustand:无 Provider 的全局状态
import { create } from "zustand";
type State = { filter: "all"|"done"|"todo"; setFilter: (f: State["filter"]) => void };
export const useTaskStore = create<State>((set) => ({
filter: "all",
setFilter: (filter) => set({ filter }),
}));
// 组件里:const filter = useTaskStore(s => s.filter); // 选择器,只订阅需要的字段
// services/useTasks.ts —— TanStack Query 管服务端状态
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
export const useTasks = () =>
useQuery({ queryKey: ["tasks"], queryFn: () => fetch("/api/tasks").then(r => r.json()) });
export const useAddTask = () => {
const qc = useQueryClient();
return useMutation({
mutationFn: (title: string) => fetch("/api/tasks", { method: "POST", body: JSON.stringify({ title }) }),
onSuccess: () => qc.invalidateQueries({ queryKey: ["tasks"] }), // 自动重新拉
});
};
React Router 7:数据路由
// main.tsx —— createBrowserRouter 配置式路由
import { createBrowserRouter, RouterProvider } from "react-router";
const router = createBrowserRouter([
{
path: "/",
element: <Layout />, // 带 <Outlet/> 的布局
errorElement: <ErrorPage />, // 路由级错误兜底
children: [
{ index: true, element: <Home /> }, // 索引路由
{ path: "tasks/:id", element: <TaskDetail />,
// loader 在进页面前就加载数据,组件里用 useLoaderData 拿
loader: async ({ params }) => fetch(`/api/tasks/${params.id}`),
},
{ path: "about", element: <About /> },
],
},
]);
createRoot(document.body).render(<RouterProvider router={router} />);
// 组件里:useParams 拿动态参数,useNavigate 编程式跳转
// import { Link, NavLink, useParams, useNavigate, useSearchParams } from "react-router";
完整案例骨架:任务管理应用(React 19 + TS + Router + Zustand + Query)
// 目录结构
src/
routes/ # 页面:Home.tsx TaskDetail.tsx NewTask.tsx
components/ # TaskList.tsx TaskItem.tsx TaskForm.tsx
stores/useTaskStore.ts
services/useTasks.ts
main.tsx # createBrowserRouter
# 核心数据流:
# 列表页 useTasks() 拿数据 → TaskList 渲染 → TaskItem 用 Zustand 的 filter
# 新建 TaskForm 用 useActionState + useFormStatus 提交
# 提交成功 invalidateQueries 自动刷新列表,全程不手 setState
本章面试题(React)
1. 为什么列表渲染要加 key?用 index 当 key 有什么问题?
看答案
key 帮 React 识别哪些项增删移,做高效 Diff。用 index 当 key:列表头部插入或中间删除时,后面所有项的 index 变了,React 误以为还是原来那批,导致状态错位(勾选状态跑错行)、DOM 复用错误。用业务稳定 id。
2. useEffect 和 useLayoutEffect 区别?
看答案
useEffect 在浏览器绘制后异步执行,不阻塞屏幕;useLayoutEffect 在 DOM 更新后、浏览器绘制前同步执行,适合读取 DOM 尺寸并同步改样式(否则用户会看到一帧闪烁)。绝大多数场景用 useEffect。
3. React.memo / useMemo / useCallback 是银弹吗?
看答案
不是。memo 做浅比较也有成本,useMemo 记住的计算本身有内存开销。原则:先用 Profiler 找到真瓶颈,再针对性优化;优先"状态下放、组件拆分、懒加载"这些结构性手段。滥用反而更慢。
4. 受控组件和非受控组件区别?
看答案
受控组件:表单值由 React state 驱动(value + onChange),可随时校验/转换,推荐。非受控组件:值存在 DOM 里,用 ref 读取(useRef + defaultValue),简单场景或集成老库时用。
5. React 19 相比 18 最大的变化?
看答案
① forwardRef 退休,ref 直接作 prop;② Actions 模式:useActionState/useFormStatus 统一异步表单;③ useOptimistic 乐观更新;④ use() 在渲染中读 Promise/Context 配 Suspense;⑤ Server Components 稳定;⑥ 文档元数据可直接写组件。
6. 虚拟 DOM 一定比直接操作 DOM 快吗?
看答案
不一定。虚拟 DOM 的价值不是"快",而是"声明式 + 可预测":你描述 UI,React 做批量最小更新。它有 Diff 开销,但换来开发效率和一致性。极致性能场景(动画、图表)该用原生 API 或 WebGL 就别硬套 React。