楼层: 首页/ 软件技术/ 前端基础/ 第 7 章 React 19:组件化的声明式 UI
应

第 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。