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>;
}

三条规则:

  1. 状态更新是异步批处理的:连续 setCount(count + 1) 三次只加 1。
  2. 需要基于上一次值时用函数式更新:setCount(c => c + 1)。
  3. 不要直接修改状态对象: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 规则:

  1. 只能在函数组件或其它 Hook 中调用(不能写在条件、循环、普通函数里)。
  2. 命名必须以 use 开头,供 ESLint 检查。
  3. 自定义 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 中调用 */ }
← 返回帖子 更多前端内容 →