POST · 前端 · 文章
React 从入门到进阶
2026/11/13 · by ZHANG XING YUN · 阅读 1 次 · 3978 字 · 预计阅读 10 分钟
本文按"组件 → 状态 → 副作用 → 性能 → 路由与状态管理 → 工程化"的顺序整理 React 的核心用法,示例基于 React 18/19 + 函数组件 + Hooks。
- 环境:React 18.2+ / 19、Vite 5+、Node 20+
- 示例默认 TypeScript 与 JSX
- 说明:React 19 的部分能力(
use、Actions、ref 作为普通 prop)在文中单独标注
一、React 是什么
React 是用于构建用户界面的库,核心思想是声明式:描述"界面在某个状态下长什么样",由 React 负责把 DOM 更新到目标状态。
三个关键机制:
| 机制 | 作用 |
|---|---|
| 组件化 | UI 拆成可复用组件,数据通过 props 单向流动 |
| 虚拟 DOM + 协调(Reconciliation) | 比较新旧树,计算最小 DOM 更新 |
| Fiber | 可中断的渲染架构,支持并发特性(时间切片、优先级) |
与 Vue 的主要差异:
| 维度 | React | Vue |
|---|---|---|
| 视图 | JSX(JS 中写 UI) | HTML 模板 + 指令 |
| 更新 | 状态变化触发组件重渲染,手动优化 | 响应式精确追踪 |
| 状态 | useState / useReducer |
ref / reactive |
| 副作用 | useEffect(依赖数组) |
watch / watchEffect |
| 优化手段 | memo / useMemo / useCallback |
computed 缓存、v-memo |
React 的心智模型是"每次渲染都是状态的快照":渲染函数里读到的是本次渲染对应的值,理解这一点能解释大部分"闭包陷阱"。
二、创建项目与 JSX
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
最小组件:
// src/App.tsx
export default function App() {
const name = "张三";
const list = ["Vue", "React", "Angular"];
return (
<div>
<h1>你好,{name}</h1>
<ul>
{list.map(item => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
2.1 JSX 规则
| 规则 | 示例 |
|---|---|
| 返回单个根节点(或 Fragment) | <><A /><B /></> |
表达式用 {} |
{count + 1} |
| 属性用 camelCase | className、onClick、htmlFor |
| 布尔属性 | <input disabled={false} /> |
| 条件渲染 | {ok ? <A /> : <B />}、{ok && <A />} |
| 列表必须带 key | {list.map(x => <li key={x.id} />)} |
| 样式对象 | style={{ color: "red" }} |
{ok && <A />} 的陷阱:ok 为 0 或 "" 时会渲染出 0 或空字符串,需要写成 {ok ? <A /> : null} 或 Boolean(ok) && ...。
2.2 渲染列表与 key
{
users.map(user => <UserCard key={user.id} user={user} />);
}
key 必须是稳定且唯一的值:
- 用 index 作为 key,在插入/删除/排序时会导致状态错位(输入框内容串行、动画异常)。
- key 只在同级列表内唯一即可。
三、组件与 props
type ButtonProps = {
children: React.ReactNode;
variant?: "primary" | "ghost";
onClick?: () => void;
};
export function Button({ children, variant = "primary", onClick }: ButtonProps) {
return (
<button className={`btn btn-${variant}`} onClick={onClick}>
{children}
</button>
);
}
要点:
- props 是只读的,组件不能修改自己的 props。
children类型用React.ReactNode,函数作为子节点时用(props) => ReactNode。- 默认值用参数解构,不要用
defaultProps(函数组件已不推荐)。
组件组合:
function Card({ header, children }: { header: React.ReactNode; children: React.ReactNode }) {
return (
<section className="card">
<header>{header}</header>
<div>{children}</div>
</section>
);
}
React 19 起,ref 可以作为普通 prop 传递,不再必须 forwardRef:
// React 19
function Input({ ref, ...rest }: { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...rest} />;
}
四、状态:useState 与 useReducer
4.1 useState
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
三条规则:
- 状态更新是异步批处理的:连续
setCount(count + 1)三次只加 1。 - 需要基于上一次值时用函数式更新:
setCount(c => c + 1)。 - 不要直接修改状态对象:
state.count++不会触发渲染。
// 错误:三次都基于同一个 count
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// 正确:函数式更新
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
4.2 对象与数组状态
const [user, setUser] = useState({ name: "张三", age: 18 });
// 更新对象:展开生成新对象
setUser(prev => ({ ...prev, age: prev.age + 1 }));
const [list, setList] = useState<string[]>([]);
// 数组:不要 push/splice
setList(prev => [...prev, "new"]);
setList(prev => prev.filter(item => item !== "x"));
setList(prev => prev.map(item => (item === "a" ? "b" : item)));
4.3 useReducer
适合状态逻辑复杂、多个子值联动的情况。
import { useReducer } from "react";
type State = { count: number; step: number };
type Action = { type: "increment" } | { type: "decrement" } | { type: "setStep"; step: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment":
return { ...state, count: state.count + state.step };
case "decrement":
return { ...state, count: state.count - state.step };
case "setStep":
return { ...state, step: action.step };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</>
);
}
useState 与 useReducer 的选择:
| 场景 | 选择 |
|---|---|
| 单个独立值 | useState |
| 多个字段联动、状态转换复杂 | useReducer |
| 需要把更新逻辑抽出去测试 | useReducer(reducer 是纯函数) |
五、副作用:useEffect
useEffect 在渲染提交之后执行,用于同步外部系统(请求、订阅、DOM 操作)。
import { useEffect, useState } from "react";
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
let cancelled = false;
async function load() {
const data = await fetchUser(userId);
if (!cancelled) setUser(data);
}
load();
return () => {
cancelled = true; // 清理:避免旧请求覆盖新状态
};
}, [userId]);
if (!user) return <p>加载中</p>;
return <p>{user.name}</p>;
}
5.1 依赖数组
| 写法 | 执行时机 |
|---|---|
useEffect(fn) |
每次渲染后都执行 |
useEffect(fn, []) |
仅挂载后执行一次 |
useEffect(fn, [a, b]) |
a 或 b 变化后执行 |
清理函数在下一次执行前与卸载时调用。
5.2 常见错误
| 错误 | 后果 | 正确做法 |
|---|---|---|
| 漏写依赖 | 闭包读到旧值(stale closure) | 补全依赖,或用 useRef 保存最新值 |
| 依赖里放对象/函数 | 每次渲染都重新执行 | 用 useMemo/useCallback 稳定引用,或改写依赖为原始值 |
在 effect 里直接 setState 无依赖 |
无限循环 | 检查依赖与更新条件 |
| 清理函数缺失 | 定时器、订阅泄漏 | 返回清理函数 |
| 把请求写在渲染体里 | 每次渲染都发请求 | 放到 useEffect 或事件处理里 |
5.3 严格模式下的双执行
开发环境 StrictMode 会故意执行两次 effect(挂载 → 卸载 → 重新挂载),用于暴露清理不彻底的问题。生产环境不会。不要为了绕过它而删掉清理逻辑。
5.4 不该用 useEffect 的场景
- 派生数据:用计算变量或
useMemo,不要用 effect + setState。 - 事件响应:直接在事件处理函数里做。
- 重置状态:用
key强制重建组件。
// 反例:用 effect 计算派生值
const [total, setTotal] = useState(0);
useEffect(() => setTotal(price * count), [price, count]);
// 正例:直接计算
const total = price * count;
六、引用:useRef
6.1 DOM 引用
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
6.2 保存可变值
useRef 的变化不会触发渲染,适合保存定时器、上一次的值、非 UI 状态。
const timerRef = useRef<number | null>(null);
useEffect(() => {
timerRef.current = window.setInterval(() => {}, 1000);
return () => {
if (timerRef.current) window.clearInterval(timerRef.current);
};
}, []);
6.3 解决闭包陷阱
function Chat() {
const [text, setText] = useState("");
const textRef = useRef(text);
textRef.current = text;
useEffect(() => {
const id = setInterval(() => {
console.log(textRef.current); // 总是最新值
}, 1000);
return () => clearInterval(id);
}, []);
return <input value={text} onChange={e => setText(e.target.value)} />;
}
七、性能:memo、useMemo、useCallback
import { memo, useMemo, useCallback, useState } from "react";
const Child = memo(function Child({ onSelect }: { onSelect: (id: number) => void }) {
console.log("Child 渲染");
return <button onClick={() => onSelect(1)}>选择</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// 稳定函数引用,避免 Child 因 props 变化而重渲染
const handleSelect = useCallback((id: number) => {
console.log(id);
}, []);
// 昂贵计算缓存
const expensive = useMemo(() => {
return Array.from({ length: 10000 }, (_, i) => i).reduce((a, b) => a + b, 0);
}, []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<p>{expensive}</p>
<Child onSelect={handleSelect} />
</>
);
}
| API | 作用 | 使用时机 |
|---|---|---|
memo |
浅比较 props,未变则跳过渲染 | 纯展示组件、频繁被父组件带动渲染 |
useMemo |
缓存计算结果 | 计算昂贵、或需要稳定引用 |
useCallback |
缓存函数引用 | 传给 memo 子组件或作为其它 Hook 依赖 |
注意:
- 不要给所有组件都套
memo,比较成本可能高于收益。 useMemo不是语义保证,React 可以在内存紧张时丢弃缓存。- 依赖数组写错会让缓存永不命中,反而更慢。
八、Context 与全局状态
8.1 Context
import { createContext, useContext, useState } from "react";
type Theme = "light" | "dark";
const ThemeContext = createContext<{ theme: Theme; toggle: () => void }>({
theme: "light",
toggle: () => {}
});
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
const toggle = () => setTheme(t => (t === "light" ? "dark" : "light"));
return <ThemeContext.Provider value={{ theme, toggle }}>{children}</ThemeContext.Provider>;
}
export const useTheme = () => useContext(ThemeContext);
性能问题:Provider 的 value 变化会让所有消费该 Context 的组件重渲染。
优化手段:
- 拆分 Context(主题、用户、权限分开)。
- 用
useMemo缓存 value。 - 高频变化的值不要放 Context(改用订阅式状态库)。
8.2 状态方案对比
| 方案 | 特点 | 适用 |
|---|---|---|
useState / useReducer |
组件内 | 局部状态 |
| Context | 跨层传递,无额外依赖 | 主题、登录态、配置 |
| Zustand | 轻量、订阅式、无需 Provider | 中小型全局状态 |
| Redux Toolkit | 规范强、DevTools 完善 | 大型项目、多人协作 |
| Jotai / Valtio / MobX | 原子化 / 代理式 | 按团队偏好 |
Zustand 示例:
import { create } from "zustand";
type UserState = {
token: string;
login: (token: string) => void;
logout: () => void;
};
export const useUserStore = create<UserState>(set => ({
token: "",
login: token => set({ token }),
logout: () => set({ token: "" })
}));
const token = useUserStore(state => state.token); // 只订阅需要的字段
九、自定义 Hook
把复用逻辑抽成 use 开头的函数:
import { useEffect, useState } from "react";
export function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(json => {
if (!cancelled) setData(json);
})
.catch(err => {
if (!cancelled) setError(err);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
Hook 规则:
- 只能在函数组件或其它 Hook 中调用(不能写在条件、循环、普通函数里)。
- 命名必须以
use开头,供 ESLint 检查。 - 自定义 Hook 之间可以互相组合。
十、路由
React Router v6/v7:
npm i react-router-dom
// main.tsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{ path: "users", element: <UserList /> },
{ path: "users/:id", element: <UserDetail /> },
{ path: "*", element: <NotFound /> }
]
}
]);
createRoot(document.getElementById("root")!).render(<RouterProvider router={router} />);
// Layout.tsx
import { Outlet, NavLink } from "react-router-dom";
export function Layout() {
return (
<>
<nav>
<NavLink to="/" end>
首页
</NavLink>
<NavLink to="/users">用户</NavLink>
</nav>
<main>
<Outlet />
</main>
</>
);
}
读取参数与跳转:
import { useParams, useSearchParams, useNavigate } from "react-router-dom";
const { id } = useParams();
const [params, setParams] = useSearchParams();
const navigate = useNavigate();
navigate(`/users/${id}`);
setParams({ page: "2" });
懒加载:
import { lazy, Suspense } from "react";
const UserDetail = lazy(() => import("./pages/UserDetail"));
<Suspense fallback={<p>加载中</p>}>
<UserDetail />
</Suspense>;
路由要点:
createBrowserRouter需要服务端把未知路径回退到index.html。- v6 移除了
Switch,改用<Routes>(或路由表)。 - 嵌套路由用
<Outlet />渲染子路由。 - 路由守卫用 loader 或组件内包装(v6.4+ 提供
loader、redirect)。
十一、表单
11.1 受控组件
function Login() {
const [form, setForm] = useState({ email: "", password: "" });
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setForm(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
login(form);
};
return (
<form onSubmit={handleSubmit}>
<input name="email" value={form.email} onChange={handleChange} />
<input name="password" type="password" value={form.password} onChange={handleChange} />
<button type="submit">登录</button>
</form>
);
}
受控组件的特点:值由 state 驱动,便于校验与联动,但每次输入都会触发渲染。
11.2 非受控组件
const emailRef = useRef<HTMLInputElement>(null);
function handleSubmit(event: React.FormEvent) {
event.preventDefault();
console.log(emailRef.current?.value);
}
适合简单表单或需要接入非 React 的表单库。复杂表单推荐 react-hook-form(非受控 + 校验,性能好)或 Formik。
十二、样式方案
| 方案 | 写法 | 特点 |
|---|---|---|
| 普通 CSS | import "./app.css" |
简单,全局作用域 |
| CSS Modules | import s from "./app.module.css" |
局部作用域,零运行时 |
| CSS-in-JS | styled.div\`` |
动态样式,运行时开销 |
| 原子化 CSS | Tailwind | 开发快,类名较长 |
| 组件库 | Ant Design / MUI | 开箱即用 |
import styles from "./Button.module.css";
export function Button() {
return <button className={styles.primary}>按钮</button>;
}
动态样式优先用 className 组合或 CSS 变量,避免大量内联 style 对象(每次渲染都是新对象)。
十三、并发特性与代码分割
| 能力 | 作用 | 示例 |
|---|---|---|
React.lazy + Suspense |
组件级代码分割 | 见路由懒加载 |
useTransition |
把更新标记为"非紧急",保持输入流畅 | 搜索过滤大列表 |
useDeferredValue |
延迟某个值的更新 | 大列表渲染 |
startTransition |
非 Hook 场景的低优先级更新 | 路由切换 |
const [isPending, startTransition] = useTransition();
const [keyword, setKeyword] = useState("");
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
setKeyword(value); // 紧急:输入框立即更新
startTransition(() => {
setFilter(value); // 非紧急:列表更新可被打断
});
}
React 19 新增:
use(promise):在组件中读取 Promise / Context。- Actions:表单
action属性 +useActionState。 useOptimistic:乐观更新。ref作为普通 prop,不再需要forwardRef。
十四、错误处理
14.1 错误边界
函数组件无法捕获渲染错误,需要类组件:
import { Component, type ErrorInfo, type ReactNode } from "react";
class ErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean }> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error("组件错误", error, info);
}
render() {
if (this.state.hasError) {
return <p>页面出错了,请刷新重试</p>;
}
return this.props.children;
}
}
错误边界只能捕获子树渲染、生命周期、构造函数中的错误,不能捕获事件处理、异步回调、SSR 中的错误。
14.2 异步错误
异步错误需要自己 try/catch 或通过全局监听:
useEffect(() => {
const handler = (event: ErrorEvent) => {
report(event.error);
};
window.addEventListener("error", handler);
window.addEventListener("unhandledrejection", handler);
return () => {
window.removeEventListener("error", handler);
window.removeEventListener("unhandledrejection", handler);
};
}, []);
十五、工程化与测试
# 测试
npm i -D vitest @testing-library/react @testing-library/jest-dom jsdom
// Button.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { Button } from "./Button";
describe("Button", () => {
it("点击时触发回调", () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>提交</Button>);
fireEvent.click(screen.getByText("提交"));
expect(onClick).toHaveBeenCalledTimes(1);
});
});
工程化建议:
- 组件目录与业务目录分离,公共组件配测试与文档。
- 统一 ESLint(
eslint-plugin-react-hooks必开,能发现依赖数组问题)。 - 使用
React.StrictMode暴露清理不彻底的问题。 - 生产构建开启代码分割与 gzip。
十六、常见坑
| 现象 | 原因 | 处理 |
|---|---|---|
| 状态更新后立刻读到旧值 | 渲染快照语义 | 用新值变量,或 useEffect 同步 |
连续 setState 只加一次 |
批处理 + 非函数式更新 | 用 setState(prev => prev + 1) |
useEffect 无限循环 |
依赖每次都是新对象/新函数 | 稳定引用或改依赖 |
| 拿到旧 state(stale closure) | 闭包捕获了旧值 | 函数式更新、useRef、补依赖 |
| 列表输入框内容错乱 | key 用了 index | 用唯一 id |
| 子组件莫名重渲染 | 父组件每次创建新对象/函数 | memo + useMemo/useCallback |
| 修改数组/对象不生效 | 直接 mutation | 生成新对象 |
| 受控组件无法输入 | value 没有配 onChange |
补 onChange 或改非受控 |
Context 更新导致整树重渲染 |
value 每次新对象 | useMemo 缓存、拆分 Context |
StrictMode 下请求发两次 |
开发环境故意双执行 | 不要删清理逻辑;用请求去重 |
十七、FAQ
Q1:useEffect 的依赖数组可以省略吗?
省略表示每次渲染都执行,通常不是想要的效果。应显式写出依赖,并用 ESLint 校验。
Q2:useMemo/useCallback 一定要用吗?
不是。只有出现性能问题、或需要稳定引用作为依赖时才用。滥用会增加复杂度。
Q3:memo 为什么没生效?
props 里包含每次新建的对象、函数或 children,浅比较永远不等。需要配合 useMemo/useCallback。
Q4:useEffect 和 useLayoutEffect 的区别?
useEffect 在浏览器绘制后执行(异步);useLayoutEffect 在 DOM 更新后、绘制前同步执行,用于测量布局、避免闪烁。
Q5:函数组件能捕获错误吗?
不能,渲染错误需要错误边界(类组件)。事件与异步错误要自己 try/catch。
Q6:状态该放组件内还是全局?
先组件内;需要跨层且多个组件共享时,再提升到 Context 或状态库。不要一上来就用全局状态。
Q7:useState 初始值传函数有什么意义?
惰性初始化:useState(() => compute()) 只在首次渲染执行,避免每次渲染都计算。
Q8:为什么表单每次输入都重渲染?
受控组件的值变化触发父组件渲染。大表单用 react-hook-form(非受控)或拆分组件。
Q9:React 19 还需要 forwardRef 吗?
不需要,ref 可以作为普通 prop 传递;但为了兼容旧版本,库代码仍常见 forwardRef。
Q10:如何做代码分割?
React.lazy + Suspense、路由级懒加载(lazy 或路由表 lazy)。注意按路由/重组件拆,不要拆得太碎。
附录:Hooks 速查
// 状态
useState(initial)
useReducer(reducer, initialState)
useOptimistic(state, updateFn) // 19
// 副作用
useEffect(fn, deps)
useLayoutEffect(fn, deps)
useInsertionEffect(fn, deps) // 样式库使用
// 引用
useRef(initial)
useImperativeHandle(ref, createHandle, deps)
// 性能
memo(Component)
useMemo(fn, deps)
useCallback(fn, deps)
// 上下文
createContext(defaultValue)
useContext(Context)
use(Context) // 19
// 并发
useTransition()
useDeferredValue(value)
startTransition(fn)
Suspense / lazy
// 自定义
function useXxx() { /* 只能在组件或 Hook 中调用 */ }