React 完全指南
作者:海川,发表于:2026年1月28日 03:00:00
从原生JS的痛点,到jQuery的改进,再到React的革新。理解技术演进的背景,掌握React的设计哲学、Hooks的应用逻辑、性能优化策略和10+实战模式
前言
很多开发者学 React 时,直接从”组件、Props、State”开始,但这样学习就像在雾中摸象——**为什么需要 React?为什么要这样设计?为什么需要各种 Hook?**都说不清楚。
本文采用技术演进路径,从原生 JavaScript 的痛点谈起,经历 jQuery 的改进,再到 React 的革新,层层递进。你会理解每个 React 特性出现的原因,而不是机械地学习语法。
一、技术演进史:为什么需要 React?
原生 HTML + CSS + JS 的黑暗时代
想象一个需求:用户点击按钮,计数器 +1,同时更新页面显示。
<!-- 原生HTML -->
<button id="btn">Click me</button>
<div id="count">Count: 0</div>
<script>
let count = 0;
document.getElementById("btn").addEventListener("click", () => {
count++;
// 手动更新 DOM
document.getElementById("count").textContent = `Count: ${count}`;
});
</script>
看起来简单,但问题开始出现:
<!-- 如果需求变复杂:多个按钮、多个状态、互相影响 -->
<button id="btn1">Count++</button>
<button id="btn2">Reset</button>
<button id="btn3">Add 10</button>
<div id="count">Count: 0</div>
<div id="status">Status: idle</div>
<div id="log">Logs:</div>
<script>
let count = 0;
let logs = [];
// ❌ 问题 1:状态分散
// count、logs 散落在全局作用域
// ❌ 问题 2:DOM 更新繁琐
// 每次状态改变都要手动更新多个 DOM 元素
const updateUI = () => {
document.getElementById("count").textContent = `Count: ${count}`;
document.getElementById("log").textContent = `Logs: ${logs.join(", ")}`;
document.getElementById("status").textContent =
count > 10 ? "Status: high" : "Status: low";
};
// ❌ 问题 3:事件监听器混乱
// 每个按钮都要单独绑定,逻辑散落各处
document.getElementById("btn1").addEventListener("click", () => {
count++;
logs.push("increment");
updateUI();
});
document.getElementById("btn2").addEventListener("click", () => {
count = 0;
logs = [];
updateUI();
});
// ❌ 问题 4:代码维护困难
// 不知道改哪个变量会影响哪些 DOM
// 添加新功能要修改多处地方
// 很容易出现状态不一致的 BUG
</script>
核心痛点:
| 痛点 | 问题 |
|---|---|
| 状态和 DOM 不同步 | 状态改了,DOM 要手动更新;DOM 改了,状态要手动保存 |
| 代码散落各处 | 同一个业务逻辑的代码分散在多个事件监听器中 |
| 维护困难 | 不清楚改一个变量会影响哪些 DOM;添加功能要触及多个地方 |
| 重复代码多 | 类似的功能要重复写很多遍 |
| 缺乏组织结构 | 大项目就是一堆”意大利面条代码”(spaghetti code) |
jQuery 时代:部分改进
jQuery 的出现(约 2006 年)改进了一些问题:
// jQuery 让 DOM 操作更简洁
$("#btn").click(function () {
count++;
$("#count").text("Count: " + count);
});
// 支持链式操作
$("#btn")
.click(function () {
count++;
})
.on("mouseenter", function () {
$(this).addClass("hover");
});
jQuery 解决的问题:
✅ DOM 操作语法更简洁($("#id"))
✅ 跨浏览器兼容
✅ 事件处理更统一
但 jQuery 仍然无法解决根本问题:
// ❌ 状态和 DOM 仍然分离
let count = 0; // 状态在这里
$("#btn").click(() => {
count++; // 改变状态
$("#count").text("Count: " + count); // 手动更新 DOM
});
// 如果忘记更新 DOM,就会出现不同步的 BUG
// ❌ 问题依然存在:状态和 DOM 是两个独立的系统
jQuery 的局限:
- 还是命令式编程(告诉计算机怎么做)
- 开发者需要手动管理状态和 DOM 的同步
- 当应用复杂时,代码仍然混乱
React 的革命:声明式 UI
React(约 2013 年)采取了完全不同的思路:
// React:让 UI 跟随状态自动更新
function Counter() {
const [count, setCount] = React.useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Click me</button>
<div>Count: {count}</div>
</div>
);
}
关键转变:
❌ 命令式(jQuery):我要找到 DOM,我要改它,我要更新它
✅ 声明式(React):当状态是 0 时,显示这样;当状态是 1 时,显示那样
React 的核心思想:
UI = f(state)
意思是:UI 是状态的函数。状态改变 → React 自动重新渲染 UI。
彻底解决的问题:
✅ 状态和 UI 同步 - 只改状态,UI 自动跟随
✅ 组件化 - 可以写可复用的组件
✅ 单向数据流 - 数据流清晰,更易调试
✅ 声明式语法 - 代码表达意图,更易理解
✅ 性能自动优化 - React 会自动找出哪些 DOM 需要更新
二、核心速览(60 秒)
React 的三层演进
| 阶段 | 特点 | 例子 |
|---|---|---|
| 原生 JS | 命令式,手动同步状态和 DOM | element.textContent = value |
| jQuery | 简化 DOM 操作,但仍命令式 | $("#id").text(value) |
| React | 声明式,UI 自动同步状态 | <div>{value}</div> |
React 核心概念速查
| 概念 | 作用 | 为什么需要 |
|---|---|---|
| JSX | 描述 UI 结构 | 比字符串模板更直观 |
| 组件 | 可复用的 UI 单元 | 避免重复代码 |
| Props | 父子通信(单向) | 组件需要参数化 |
| State | 组件内部可变数据 | 应对用户交互 |
| Hooks | 函数式组件的状态和副作用 | 避免类组件的复杂性 |
三、JSX 和组件:UI 的声明方式
为什么需要 JSX?
在没有 JSX 之前:
// ❌ 原生 JS 写法:字符串拼接(易出错、难维护)
const html = `
<div class="container">
<h1>${title}</h1>
<p>${description}</p>
</div>
`;
// ❌ 或者手动创建 DOM(冗长繁琐)
const div = document.createElement("div");
div.className = "container";
const h1 = document.createElement("h1");
h1.textContent = title;
div.appendChild(h1);
JSX 解决的问题:
✅ 使用 HTML 语法来描述 UI(直观)
✅ 支持 JavaScript 表达式(灵活)
✅ 编译检查(防止错误)
// ✅ JSX 写法:清晰简洁
const element = (
<div className="container">
<h1>{title}</h1>
<p>{description}</p>
</div>
);
JSX 的本质
JSX 不是字符串,而是 JavaScript 语法糖:
// JSX
const element = <div className="box">Hello {name}</div>;
// 编译后(实际运行的代码)
const element = React.createElement(
"div",
{ className: "box" },
"Hello ",
name,
);
理解这一点很重要:JSX 的每个 {} 都是真正的 JavaScript 表达式。
// ✅ 可以使用任意 JS 表达式
const element = (
<div>
<h1>{2 + 2}</h1>
<p>{isLoggedIn ? "Welcome" : "Please login"}</p>
<button onClick={() => console.log("clicked")}>Click</button>
</div>
);
组件:代码复用的基石
原生 JS 的困境:
// ❌ 如果要多个计数器,怎么办?
// 方案 1:复制粘贴代码(维护地狱)
let count1 = 0;
let count2 = 0;
let count3 = 0;
document.getElementById("btn1").addEventListener("click", () => {
count1++;
document.getElementById("display1").textContent = count1;
});
document.getElementById("btn2").addEventListener("click", () => {
count2++;
document.getElementById("display2").textContent = count2;
});
// ... 如果要 100 个计数器,就要写 100 遍?
React 的解决方案:组件
// ✅ 定义一个可复用的组件
function Counter({ title }) {
const [count, setCount] = React.useState(0);
return (
<div>
<h2>{title}</h2>
<button onClick={() => setCount(count + 1)}>+</button>
<p>Count: {count}</p>
</div>
);
}
// 直接复用,无需重复代码
function App() {
return (
<div>
<Counter title="Counter 1" />
<Counter title="Counter 2" />
<Counter title="Counter 3" />
</div>
);
}
组件的本质:
组件 = 函数 + JSX + 状态管理
四、Props 和 State:数据流的两层
为什么需要分开 Props 和 State?
jQuery 时代的问题:
// ❌ 数据和 DOM 混乱
function createButton(label, value) {
// label 是配置,value 是状态
// 两者混在一起,分不清
const btn = $(`<button>${label}</button>`);
btn.data("value", value); // 把数据藏在 DOM 里
btn.click(() => {
// 改了状态,DOM 不会自动更新
value++;
});
return btn;
}
React 的清晰设计:
// ✅ Props:配置信息(父组件→子组件,不可修改)
function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
// ✅ State:可变数据(组件内部管理)
function Counter() {
const [count, setCount] = React.useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<p>Count: {count}</p>
</div>
);
}
Props 的单向流动
为什么 Props 不能从子组件修改?
// ❌ 为什么这样设计是错的?
function Child({ name }) {
name = "Bob"; // 直接修改 Props
// 问题 1:父组件不知道这个改变
// 问题 2:其他使用这个组件的地方也会受到影响
// 问题 3:数据流变得混乱,难以调试
}
// ✅ 正确做法:通过回调函数告诉父组件
function Child({ name, onNameChange }) {
return <input value={name} onChange={(e) => onNameChange(e.target.value)} />;
}
function Parent() {
const [name, setName] = React.useState("Alice");
return <Child name={name} onNameChange={setName} />;
}
单向数据流的优势:
✅ 数据流清晰:下一层组件需要什么,父组件显式提供
✅ 易于调试:改了什么,直接看父组件的 Props
✅ 防止 BUG:不会有意外的数据修改
State:为什么需要 setState?
初学者常犯的错误:
function Counter() {
let count = 0; // ❌ 普通变量
return (
<>
<button
onClick={() => {
count++; // 改变了,但 UI 不会更新!
}}
>
+
</button>
<p>Count: {count}</p>
</>
);
}
// 为什么?因为 React 不知道 count 改变了
// React 只在调用 setState 时才会重新渲染
React 的设计:
function Counter() {
const [count, setCount] = React.useState(0);
// 为什么这样设计?
return (
<button
onClick={() => {
setCount(count + 1); // 通知 React:"状态改变了,需要重新渲染"
}}
>
Count: {count}
</button>
);
}
setState 的两个作用:
- 通知 React:状态改变了,需要重新渲染
- 批量更新:React 会合并多个 setState,避免频繁重渲染
// React 会将这三个 setState 合并为一次渲染
function handleClick() {
setCount(count + 1);
setTotal(total + 1);
setLog([...log, "clicked"]);
// React 不会渲染 3 次,而是渲染 1 次
// 这样性能更好
}
五、为什么需要 Hooks?从类组件的困境说起
类组件时代的问题
React 最初使用类组件来管理状态:
// 类组件(ES6 之前的标准做法)
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
// ❌ 问题 1:必须绑定 this
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
// ❌ 问题 2:生命周期方法分散逻辑
componentDidMount() {
// 初始化逻辑
const subscription = subscribe();
this.subscription = subscription;
}
componentDidUpdate(prevProps) {
// 更新逻辑,但和 componentDidMount 做的事重复了
if (prevProps.userId !== this.props.userId) {
const subscription = subscribe();
this.subscription = subscription;
}
}
componentWillUnmount() {
// 清理逻辑
this.subscription.unsubscribe();
}
render() {
return (
<div>
<button onClick={this.handleClick}>+</button>
<p>Count: {this.state.count}</p>
</div>
);
}
}
类组件的五大困境:
| 困境 | 问题描述 |
|---|---|
| this 绑定 | 方法需要绑定 this,容易出错 |
| 逻辑分散 | 相关逻辑被分散到不同的生命周期方法 |
| 代码重复 | mount 和 update 经常需要重复相同的逻辑 |
| 难以复用 | 状态逻辑无法有效提取和复用 |
| 代码冗长 | 即使简单功能也要写很多模板代码 |
Hooks:函数式的优雅解决方案
React 16.8(2019 年)推出 Hooks,彻底改变了这一切:
// ✅ 函数式组件 + Hooks:简洁优雅
function Counter() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
// 初始化 + 更新逻辑都在这里
console.log("订阅数据");
return () => {
// 清理逻辑
console.log("取消订阅");
};
}, []); // 依赖数组控制执行时机
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
Hooks 的优势:
✅ 代码简洁 - 70% 的代码量减少
✅ 逻辑组织 - 相关逻辑集中在一个 Hook 中
✅ 易于复用 - 可以提取自定义 Hooks
✅ 没有 this 问题 - 函数式编程避免 this 绑定
✅ 组合优于继承 - 通过 Hooks 组合实现复杂逻辑
六、useState:管理组件状态
useState 出现的原因
类组件的痛点:
// ❌ 类组件:状态和方法混在一起
class Counter extends React.Component {
state = { count: 0, name: "", items: [] };
increment = () => this.setState({ count: this.state.count + 1 });
setName = (name) => this.setState({ name });
addItem = (item) => this.setState({ items: [...this.state.items, item] });
// 问题:状态多了以后,很难追踪哪个方法改哪个状态
}
useState 的解决方案:
// ✅ 每个状态独立管理
function Counter() {
const [count, setCount] = React.useState(0);
const [name, setName] = React.useState("");
const [items, setItems] = React.useState([]);
// 清晰明了:每个 useState 管理一个状态
}
为什么需要依赖项数组?
初学者困惑:为什么 count 改变了,setCount 就要重新执行?
function Counter() {
const [count, setCount] = React.useState(0);
// ❌ 问题:如果没有依赖数组,这会无限循环
React.useEffect(() => {
setCount(count + 1); // 改变 count
}); // 没有依赖数组 → 每次渲染都执行 → count 改变 → 重新渲染 → 无限循环
// ✅ 解决:指定依赖数组
React.useEffect(() => {
// 只在挂载时执行一次
setCount(count + 1);
}, []); // 空数组 = 只执行一次
}
七、useEffect:处理副作用
什么是副作用?为什么需要特殊处理?
纯函数 - 没有副作用:
// ✅ 纯函数:相同输入,总是相同输出,无副作用
function add(a, b) {
return a + b;
}
有副作用的函数 - 需要 useEffect 处理:
// ❌ 副作用:修改全局变量、发送网络请求、修改 DOM 等
function Component() {
// 这些应该在 useEffect 里,而不是在 render 时执行
const data = fetch("/api/data"); // 发送网络请求
localStorage.setItem("count", count); // 修改本地存储
console.log("Component rendered"); // 副作用
}
useEffect 的作用:控制何时执行副作用
function Component() {
const [count, setCount] = React.useState(0);
// ✅ 只在挂载和卸载时执行一次
React.useEffect(() => {
const timer = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
return () => clearInterval(timer); // 清理
}, []);
// ✅ 每当 count 改变时执行
React.useEffect(() => {
console.log(`Count is now ${count}`);
}, [count]);
// ✅ 获取用户数据,当 userId 改变时重新获取
React.useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]);
}
useEffect 解决的根本问题:
❌ jQuery 时代:代码在任何时候都可能执行,很难控制
✅ React + useEffect:明确指定副作用何时执行
八、useCallback 和 useMemo:为什么需要性能优化?
问题:函数引用改变导致的重渲染
没有 useCallback 时:
function Parent() {
const [count, setCount] = React.useState(0);
// 每次 Parent 渲染,这个函数都被重新创建
const handleClick = () => {
console.log(count);
};
// 子组件认为 onClick 改变了(引用不同),会重新渲染
return <Child onClick={handleClick} />;
}
const Child = React.memo(({ onClick }) => {
console.log("Child rendered"); // 会不必要地重新渲染
return <button onClick={onClick}>Click</button>;
});
useCallback 的目的:保持函数引用不变
function Parent() {
const [count, setCount] = React.useState(0);
// ✅ 当依赖不变时,保持相同的函数引用
const handleClick = React.useCallback(() => {
console.log(count);
}, [count]); // 只有当 count 改变时,才创建新函数
// Child 的 onClick 引用没变,不会重新渲染
return <Child onClick={handleClick} />;
}
为什么需要 useMemo?
没有 useMemo 时:
function Component({ items }) {
// 每次渲染都重新排序(即使 items 没变)
const sortedItems = items.sort((a, b) => a - b);
return <List items={sortedItems} />;
}
useMemo 的目的:缓存昂贵的计算
function Component({ items }) {
// ✅ 只有当 items 改变时,才重新计算
const sortedItems = React.useMemo(() => {
return items.sort((a, b) => a - b);
}, [items]);
return <List items={sortedItems} />;
}
九、useRef:访问 DOM 和保存值
useRef 出现的原因
什么时候需要访问 DOM?
// ❌ 在 React 中,应该尽量避免直接操作 DOM
// 但有些场景无法避免
function TextInput() {
// 场景 1:焦点管理(无法用 React 实现)
const handleFocus = () => {
// 如何让 input 获得焦点?需要访问 DOM
};
// 场景 2:播放/暂停视频(需要调用 DOM API)
const handlePlay = () => {
// 如何播放视频?需要访问 video 元素
};
// 场景 3:获取 input 值(通常用 useState,但某些场景需要 ref)
const handleSubmit = () => {
// 如何获取 input 的值?
};
}
useRef 的解决方案:
function TextInput() {
const inputRef = React.useRef(null);
const handleFocus = () => {
inputRef.current.focus(); // 访问真实的 DOM 元素
};
return (
<>
<input ref={inputRef} />
<button onClick={handleFocus}>Focus</button>
</>
);
}
useRef 的另一个用途:保存不需要重渲染的值
// ❌ 问题:使用普通变量
function Stopwatch() {
let intervalId; // 每次渲染都重新创建
const start = () => {
intervalId = setInterval(() => {
// ...
}, 100);
};
const stop = () => {
clearInterval(intervalId); // 可能是 undefined(因为 intervalId 被重置)
};
}
// ✅ 解决:使用 useRef
function Stopwatch() {
const intervalRef = React.useRef(null);
const start = () => {
intervalRef.current = setInterval(() => {
// ...
}, 100);
};
const stop = () => {
clearInterval(intervalRef.current); // 总是能获取到正确的 ID
};
}
useRef 的特点:
✅ 返回的对象在组件整个生命周期内保持不变
✅ 修改 ref 不会触发重渲染
✅ 可以用来保存任何值
十、useContext:跨层级通信的必要性
Props Drilling 的困境
想象这样的组件树:
App
├─ Header
│ └─ Nav
│ └─ UserMenu
│ └─ Avatar(需要 theme)
如果 App 有一个 theme 状态,Avatar 需要使用它:
// ❌ Props Drilling:theme 要一层层向下传递
function App() {
const [theme, setTheme] = React.useState("light");
return <Header theme={theme} />;
}
function Header({ theme }) {
return <Nav theme={theme} />; // 只是转发 Props
}
function Nav({ theme }) {
return <UserMenu theme={theme} />; // 只是转发 Props
}
function UserMenu({ theme }) {
return <Avatar theme={theme} />; // 只是转发 Props
}
function Avatar({ theme }) {
return <div className={theme}>Avatar</div>;
}
问题:
❌ 中间层(Header、Nav、UserMenu)不需要 theme,但要转发
❌ 如果增加更多全局状态(language、user),Props 会爆炸
❌ 代码冗余,维护困难
useContext 的优雅解决
// 1. 创建 Context
const ThemeContext = React.createContext();
// 2. 提供者:在顶层提供值
function App() {
const [theme, setTheme] = React.useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
</ThemeContext.Provider>
);
}
// 3. 中间层不需要任何改变
function Header() {
return <Nav />;
}
function Nav() {
return <UserMenu />;
}
// 4. 需要的组件直接获取
function Avatar() {
const { theme } = React.useContext(ThemeContext);
return <div className={theme}>Avatar</div>;
}
useContext 解决的问题:
✅ 避免 Props Drilling
✅ 全局共享数据(主题、语言、用户信息等)
✅ 中间层组件无需关心
十一、自定义 Hooks:逻辑复用的最高形式
为什么需要自定义 Hooks?
类组件时代,逻辑复用很困难:
// ❌ 高阶组件(HOC)的方式很冗长
function withFetchData(Component) {
return function WrappedComponent(props) {
const [data, setData] = React.useState(null);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
fetch(props.url)
.then((res) => res.json())
.then((data) => {
setData(data);
setLoading(false);
});
}, [props.url]);
return <Component data={data} loading={loading} {...props} />;
};
}
function UserList(props) {
return props.loading ? <div>Loading...</div> : <div>{props.data}</div>;
}
const UserListWithFetch = withFetchData(UserList);
自定义 Hooks 更简洁:
// ✅ 自定义 Hook:只是一个函数
function useFetch(url) {
const [data, setData] = React.useState(null);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((data) => {
setData(data);
setLoading(false);
});
}, [url]);
return { data, loading };
}
// 直接使用
function UserList({ url }) {
const { data, loading } = useFetch(url);
return loading ? <div>Loading...</div> : <div>{data}</div>;
}
自定义 Hooks 的优势:
✅ 简洁清晰
✅ 易于测试
✅ 易于共享
✅ 组合灵活
常见自定义 Hooks
// useFetch:数据获取
function useFetch(url) {
const [data, setData] = React.useState(null);
const [error, setError] = React.useState(null);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
fetch(url)
.then((res) => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, error, loading };
}
// useDebounce:防抖
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = React.useState(value);
React.useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// useLocalStorage:本地存储
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = React.useState(() => {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
});
const setValue = (value) => {
setStoredValue(value);
localStorage.setItem(key, JSON.stringify(value));
};
return [storedValue, setValue];
}
// usePrevious:保存前一个值
function usePrevious(value) {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
十二、状态管理进阶:从 useState 到 useReducer
核心速览:Hooks 的演进树
useState(基础状态)
├─ 单个值:const [count, setCount] = useState(0)
└─ 缺陷:多个相关状态容易混乱
useEffect(副作用管理)
├─ 替代生命周期方法
└─ 依赖数组控制执行时机
useReducer(复杂状态)
├─ 当 useState 不够用时
└─ 多个相关状态的首选
useCallback + useMemo(性能优化)
├─ 缓存函数引用和计算结果
└─ 避免不必要的重渲染
useRef(DOM 访问和值保存)
└─ 访问真实 DOM 或保存引用
useContext(跨层通信)
└─ 避免 Props Drilling
当 useState 不够用时
当有多个相关的状态时,useState 会变得混乱:
// ❌ useState 管理多个相关状态
function TodoList() {
const [todos, setTodos] = React.useState([]);
const [filter, setFilter] = React.useState("all");
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState(null);
const [sortBy, setSortBy] = React.useState("date");
// 问题:状态之间有逻辑关系
// 改 todos 时,需要同时改 loading 和 error
// 很容易遗漏某个状态
const fetchTodos = async () => {
setLoading(true); // 如果忘记这行,BUG 就出现了
try {
const data = await fetch("/api/todos");
setTodos(data);
setError(null);
} catch (err) {
setError(err);
setLoading(false);
}
setLoading(false);
};
}
useReducer 的解决方案:
// ✅ useReducer:相关状态统一管理
function TodoList() {
const [state, dispatch] = React.useReducer(todoReducer, {
todos: [],
filter: "all",
loading: false,
error: null,
sortBy: "date",
});
const fetchTodos = async () => {
dispatch({ type: "FETCH_START" });
try {
const data = await fetch("/api/todos");
dispatch({ type: "FETCH_SUCCESS", payload: data });
} catch (err) {
dispatch({ type: "FETCH_ERROR", payload: err });
}
};
}
function todoReducer(state, action) {
switch (action.type) {
case "FETCH_START":
return { ...state, loading: true, error: null };
case "FETCH_SUCCESS":
return {
...state,
todos: action.payload,
loading: false,
};
case "FETCH_ERROR":
return { ...state, loading: false, error: action.payload };
case "SET_FILTER":
return { ...state, filter: action.payload };
default:
return state;
}
}
useReducer 的优势:
✅ 相关状态统一管理,一次 dispatch 更新多个状态
✅ 状态转移逻辑清晰(纯函数 reducer)
✅ 易于测试(reducer 是纯函数)
✅ 易于调试(每个 action 都能追踪)
十三、性能优化:为什么重渲染会成为问题?
React 的重渲染机制
React 何时重渲染?
function App() {
const [count, setCount] = React.useState(0);
// ❌ 每当 count 改变,整个 App 组件都会重新渲染
// 包括下面的所有子组件
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ExpensiveComponent /> {/* 不需要重渲染,但会重新渲染 */}
<HeavyList items={items} /> {/* 不需要重渲染,但会重新渲染 */}
</div>
);
}
为什么是问题?
function ExpensiveComponent() {
console.log("ExpensiveComponent rendered"); // 每次都打印
// 假设这是一个复杂的计算
const result = fibonacci(40); // 耗时操作
return <div>{result}</div>;
}
React.memo:防止不必要的重渲染
什么时候需要 React.memo?
// ❌ 没有 React.memo
const Header = ({ title }) => {
console.log("Header rendered");
return <h1>{title}</h1>;
};
function App() {
const [count, setCount] = React.useState(0);
return (
<>
<Header title="My App" /> {/* 即使 title 没变,也会重新渲染 */}
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
</>
);
}
使用 React.memo:
// ✅ React.memo:只有 props 改变才重渲染
const Header = React.memo(({ title }) => {
console.log("Header rendered"); // 只在 title 改变时打印
return <h1>{title}</h1>;
});
function App() {
const [count, setCount] = React.useState(0);
return (
<>
<Header title="My App" /> {/* title 没变,Header 不会重新渲染 */}
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
</>
);
}
性能优化的完整方案
// ✅ React.memo + useCallback 的组合
function App() {
const [count, setCount] = React.useState(0);
const [name, setName] = React.useState("Alice");
// ✅ useCallback:缓存函数引用
const handleIncrement = React.useCallback(() => {
setCount((c) => c + 1);
}, []); // 没有依赖,函数永不改变
return (
<div>
<Header name={name} />
{/*
即使 count 改变,Header 的 props 没变
加上 React.memo,Header 不会重新渲染
*/}
<Button onClick={handleIncrement}>Count: {count}</Button>
</div>
);
}
const Header = React.memo(({ name }) => <h1>Hello {name}</h1>);
const Button = React.memo(({ onClick, children }) => (
<button onClick={onClick}>{children}</button>
));
useMemo:缓存计算结果
// ❌ 没有 useMemo
function ProductList({ products, filter }) {
// 每次渲染都重新排序和过滤(即使 products 没变)
const filtered = products
.filter((p) => p.category === filter)
.sort((a, b) => a.price - b.price);
return <List items={filtered} />;
}
// ✅ useMemo:缓存计算结果
function ProductList({ products, filter }) {
const filtered = React.useMemo(() => {
return products
.filter((p) => p.category === filter)
.sort((a, b) => a.price - b.price);
}, [products, filter]); // 只有这两个改变才重新计算
return <List items={filtered} />;
}
十四、10+ 实战模式
1. 受控表单 vs 非受控表单
// ❌ 非受控表单:DOM 保存状态,React 无法控制
function LoginForm() {
const handleSubmit = (e) => {
e.preventDefault();
const email = document.getElementById("email").value; // 从 DOM 读取
// 问题:React 不知道表单值
};
return (
<form onSubmit={handleSubmit}>
<input id="email" type="email" />
<button>Login</button>
</form>
);
}
// ✅ 受控表单:React 保存状态
function LoginForm() {
const [form, setForm] = React.useState({ email: "", password: "" });
const handleChange = (e) => {
const { name, value } = e.target;
setForm({ ...form, [name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
// form 就是当前的表单数据
console.log(form);
};
return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={form.email}
onChange={handleChange}
placeholder="Email"
/>
<input
name="password"
type="password"
value={form.password}
onChange={handleChange}
placeholder="Password"
/>
<button>Login</button>
</form>
);
}
2. 自动保存草稿
function Editor() {
const [content, setContent] = React.useState(() => {
// 从 localStorage 恢复
const saved = localStorage.getItem("draft");
return saved || "";
});
// 自动保存
React.useEffect(() => {
const timer = setTimeout(() => {
localStorage.setItem("draft", content);
}, 1000); // 1 秒无输入后保存
return () => clearTimeout(timer);
}, [content]);
return (
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Start typing..."
/>
);
}
3. 防抖搜索
function SearchUsers() {
const [query, setQuery] = React.useState("");
const [results, setResults] = React.useState([]);
// 防抖:延迟 500ms 后执行搜索
React.useEffect(() => {
const timer = setTimeout(() => {
if (query) {
fetch(`/api/search?q=${query}`)
.then((res) => res.json())
.then(setResults);
}
}, 500);
return () => clearTimeout(timer);
}, [query]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<ul>
{results.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
4. 数据获取和错误处理
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
React.useEffect(() => {
let isMounted = true; // 防止内存泄漏
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => {
if (isMounted) setUser(data);
})
.catch((err) => {
if (isMounted) setError(err);
})
.finally(() => {
if (isMounted) setLoading(false);
});
return () => {
isMounted = false; // 清理:标记组件已卸载
};
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user.name}</div>;
}
5. 模态框管理
function useModal(initialOpen = false) {
const [isOpen, setIsOpen] = React.useState(initialOpen);
return {
isOpen,
open: () => setIsOpen(true),
close: () => setIsOpen(false),
toggle: () => setIsOpen((v) => !v),
};
}
function App() {
const modal = useModal();
return (
<>
<button onClick={modal.open}>Open Modal</button>
{modal.isOpen && (
<div className="modal-overlay" onClick={modal.close}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2>Modal Title</h2>
<button onClick={modal.close}>Close</button>
</div>
</div>
)}
</>
);
}
6. 分页
function usePagination(items, pageSize = 10) {
const [currentPage, setCurrentPage] = React.useState(1);
const totalPages = Math.ceil(items.length / pageSize);
const startIndex = (currentPage - 1) * pageSize;
const currentItems = items.slice(startIndex, startIndex + pageSize);
return {
currentPage,
totalPages,
currentItems,
goToPage: (page) => {
const validPage = Math.max(1, Math.min(page, totalPages));
setCurrentPage(validPage);
},
};
}
function ItemList({ items }) {
const { currentItems, currentPage, totalPages, goToPage } = usePagination(
items,
5,
);
return (
<>
<ul>
{currentItems.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<div>
<button onClick={() => goToPage(currentPage - 1)}>Prev</button>
<span>
{currentPage} / {totalPages}
</span>
<button onClick={() => goToPage(currentPage + 1)}>Next</button>
</div>
</>
);
}
7. 全局主题管理
const ThemeContext = React.createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = React.useState(() => {
return localStorage.getItem("theme") || "light";
});
React.useEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("theme", theme);
}, [theme]);
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
function useTheme() {
return React.useContext(ThemeContext);
}
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header>
<h1>My App ({theme})</h1>
<button onClick={toggleTheme}>Toggle Theme</button>
</header>
);
}
8. 列表过滤和排序
function ProductList({ products }) {
const [filter, setFilter] = React.useState("");
const [sortBy, setSortBy] = React.useState("name");
const filtered = React.useMemo(() => {
return products
.filter((p) => p.name.toLowerCase().includes(filter.toLowerCase()))
.sort((a, b) => {
if (sortBy === "name") return a.name.localeCompare(b.name);
if (sortBy === "price") return a.price - b.price;
return 0;
});
}, [products, filter, sortBy]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter..."
/>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
<ul>
{filtered.map((p) => (
<li key={p.id}>
{p.name} - ${p.price}
</li>
))}
</ul>
</div>
);
}
9. 无限滚动
function useInfiniteScroll(callback) {
const observerTarget = React.useRef(null);
React.useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
callback();
}
});
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => observer.disconnect();
}, [callback]);
return observerTarget;
}
function InfiniteList() {
const [items, setItems] = React.useState([]);
const [page, setPage] = React.useState(1);
const [hasMore, setHasMore] = React.useState(true);
const loadMore = React.useCallback(() => {
if (!hasMore) return;
fetch(`/api/items?page=${page}`)
.then((res) => res.json())
.then((data) => {
setItems((prev) => [...prev, ...data.items]);
setHasMore(data.hasMore);
setPage((p) => p + 1);
});
}, [page, hasMore]);
const targetRef = useInfiniteScroll(loadMore);
return (
<>
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<div ref={targetRef}>Loading more...</div>
</>
);
}
10. 错误边界和回退 UI
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// 可以在这里记录错误
console.error("Error caught:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div>
<h1>Something went wrong</h1>
<p>{this.state.error?.message}</p>
</div>
);
}
return this.props.children;
}
}
function App() {
return (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
);
}
11. 组件懒加载
const HeavyComponent = React.lazy(() => import("./HeavyComponent"));
function App() {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</React.Suspense>
);
}
十五、常见陷阱和解决方案
❌ 陷阱 1:闭包陷阱
// ❌ 错误:count 总是 0
function Component() {
const [count, setCount] = React.useState(0);
const handleClick = React.useCallback(() => {
console.log(count); // 总是 0
}, []); // 缺少 count 依赖
return (
<>
<button onClick={handleClick}>Log Count</button>
<button onClick={() => setCount(count + 1)}>Increment</button>
</>
);
}
// ✅ 正确
const handleClick = React.useCallback(() => {
console.log(count);
}, [count]); // 包含 count
❌ 陷阱 2:直接修改 State
// ❌ 错误
const [user, setUser] = React.useState({ name: "Alice" });
user.name = "Bob"; // 不会触发重渲染
// ✅ 正确
setUser({ ...user, name: "Bob" });
// ❌ 错误:修改数组
const [items, setItems] = React.useState([1, 2, 3]);
items.push(4); // 不会触发重渲染
// ✅ 正确
setItems([...items, 4]);
❌ 陷阱 3:忘记依赖数组
// ❌ 无限循环
function Component() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
setCount(count + 1); // count 改变 → effect 重新运行 → count 改变 → ...
}); // 没有依赖数组
}
// ✅ 正确
React.useEffect(() => {
setCount(count + 1);
}, []); // 只运行一次
❌ 陷阱 4:列表 key 错误
// ❌ 错误:用 index 作为 key
items.map((item, index) => <li key={index}>{item}</li>);
// 问题:当列表顺序改变时,React 会错误地复用 DOM
// ✅ 正确:用唯一标识符
items.map((item) => <li key={item.id}>{item}</li>);
❌ 陷阱 5:在条件中调用 Hook
// ❌ 错误
function Component({ show }) {
if (show) {
const [count, setCount] = React.useState(0); // Hook 顺序改变
}
}
// ✅ 正确
function Component({ show }) {
const [count, setCount] = React.useState(0);
// 在条件中使用 count,不是在条件中调用 Hook
if (show) {
console.log(count);
}
}
❌ 陷阱 6:忘记清理副作用
// ❌ 错误:内存泄漏
function Component() {
React.useEffect(() => {
const timer = setInterval(() => {
console.log("tick");
}, 1000);
// 忘记清理 timer
}, []);
}
// ✅ 正确
React.useEffect(() => {
const timer = setInterval(() => {
console.log("tick");
}, 1000);
return () => clearInterval(timer); // 清理
}, []);
十六、最佳实践总结
1. 状态管理原则
// ✅ 原则 1:尽可能将状态放在低处
function Parent() {
return (
<div>
<Child1 /> {/* Child1 有自己的状态 */}
<Child2 /> {/* Child2 有自己的状态 */}
</div>
);
}
// ❌ 反面:将所有状态都放在顶层(Props Drilling)
function Parent() {
const [state1, setState1] = React.useState(0);
const [state2, setState2] = React.useState(0);
return (
<div>
<Child1 state={state1} setState={setState1} />
<Child2 state={state2} setState={setState2} />
</div>
);
}
2. 使用自定义 Hooks 提取逻辑
// ✅ 将相关逻辑提取到自定义 Hook
function useUserData(userId) {
const [user, setUser] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
React.useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
return { user, loading, error };
}
// 组件保持简洁
function UserProfile({ userId }) {
const { user, loading, error } = useUserData(userId);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user.name}</div>;
}
3. 合理使用 Context
// ✅ Context 适合:全局配置、主题、用户信息
const UserContext = React.createContext();
function App() {
const [user, setUser] = React.useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
<Header />
<Main />
<Footer />
</UserContext.Provider>
);
}
// ❌ Context 不适合:频繁变化的状态(如表单输入)
// 因为任何改变都会导致所有消费者重渲染
4. 避免过度优化
// ❌ 过度优化:为不必要的东西用 useMemo
const name = React.useMemo(() => user.name, [user]);
// ✅ 适度优化:只优化昂贵的计算
const sortedItems = React.useMemo(() => {
return items.sort((a, b) => a.priority - b.priority);
}, [items]);
5. Props 类型检查(TypeScript 或 PropTypes)
// 使用 TypeScript
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
}
function Button({ label, onClick, disabled = false }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
}
十七、总结:React 学习路径
核心概念回顾
| 概念 | 为什么需要 | 何时使用 |
|---|---|---|
| JSX | 声明式描述 UI | 所有组件 |
| 组件 | 代码复用 | 拆分 UI |
| Props | 参数化组件 | 父→子通信 |
| State | 响应用户交互 | 可变数据 |
| useState | 管理简单状态 | 单个或少数独立状态 |
| useEffect | 控制副作用执行时机 | 所有副作用 |
| useCallback/useMemo | 避免不必要重渲染 | 性能优化 |
| useContext | 避免 Props Drilling | 全局状态 |
| useReducer | 管理复杂状态逻辑 | 多个相关状态 |
| useRef | 访问 DOM 和保存值 | DOM 操作、引用保存 |
技术演进的完整链条
原生 JavaScript(命令式)
↓ 问题:状态和 DOM 分离,维护困难
jQuery(改进的 DOM 操作)
↓ 问题:仍然手动同步状态和 DOM
React(声明式,UI 自动同步状态)
├─ JSX:直观的 UI 描述
├─ 组件:可复用的 UI 单元
├─ Props + State:数据流清晰
└─ Hooks:函数式的优雅方案
├─ useState:基础状态
├─ useEffect:副作用管理
├─ useCallback/useMemo:性能优化
├─ useContext:跨层通信
└─ useReducer:复杂状态
学习建议
第一阶段(基础):理解为什么需要 React,掌握 JSX、组件、Props、State
第二阶段(进阶):深入理解每个 Hook 出现的背景,学会自定义 Hooks
第三阶段(高阶):性能优化、状态管理模式、实战项目开发
常见错误 Top 10
- ❌ 直接修改 State → ✅ 用 setState
- ❌ 用 index 作为列表 key → ✅ 用唯一标识符
- ❌ 在条件中调用 Hook → ✅ 在顶层调用
- ❌ 缺少 useEffect 依赖 → ✅ 包含所有使用的外层变量
- ❌ 对象/数组作为依赖 → ✅ 用 useMemo 缓存
- ❌ 忘记清理副作用 → ✅ 在 useEffect 中 return 清理函数
- ❌ Props Drilling → ✅ 用 useContext
- ❌ 过度优化 → ✅ 只优化有性能问题的地方
- ❌ 在 useCallback 中忽视依赖 → ✅ 包含所有使用的外层变量
- ❌ 异步操作在 useEffect 外 → ✅ 放在 useEffect 中
快速检查清单
编写 React 组件时,问自己:
// 状态管理
□ 状态放在合适的位置了吗?
□ 用了正确的状态管理方式(useState/useReducer)?
□ 有没有直接修改 State?
// 副作用
□ 副作用都在 useEffect 中吗?
□ 依赖数组正确吗?
□ 有没有忘记清理?
// 性能
□ 有没有不必要的重渲染?
□ 列表有正确的 key 吗?
□ 有没有过度优化?
// 数据流
□ 父子通信清晰吗?
□ 有没有 Props Drilling?
□ 用了合适的通信方式(Props/回调/Context)?
// 错误处理
□ 有没有处理加载状态?
□ 有没有处理错误状态?
□ 有没有处理边界情况?
后记
React 的强大不在于语法多复杂,而在于它提供了一套清晰的思想体系:
- 声明式编程 - 描述 UI 应该是什么,而不是如何改变
- 单向数据流 - 数据从上到下,事件从下到上
- 组件化 - UI 分解为独立、可复用的单元
- 函数式编程 - Hooks 让副作用和状态管理变得优雅
当你遵循这些原则时,代码会自然而然地变得清晰、可维护、高效。
现在,你已经理解了 React 背后的为什么,而不仅仅是怎么用。Go build something amazing! 🚀