# 👋 React 题目整理
# React 生命周期
# 类组件
生命周期流程:挂载时 constructor → render → componentDidMount;更新时 render → componentDidUpdate;卸载时 componentWillUnmount。
触发重新渲染:setState、props 变化或父组件渲染都会触发 render(父组件更新会导致所有子组件重新渲染,可用 PureComponent 或 shouldComponentUpdate 优化)。
// 类组件
import React, { Component } from 'react';
class LifecycleDemo extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.log('Component is mounted.');
}
componentDidUpdate(prevProps, prevState) {
console.log('Component updated. Previous count: ' + prevState.count);
}
componentWillUnmount() {
console.log('Component will unmount.');
}
render() {
return (
...
);
}
}
export default LifecycleDemo;
# 函数式组件
使用 React Hooks 来模拟类组件的生命周期:
import { useState, useEffect, useRef } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
const isMounted = useRef(false);
// componentDidMount
useEffect(() => {
console.log('组件挂载');
isMounted.current = true;
// componentWillUnmount
return () => {
console.log('组件卸载');
isMounted.current = false;
};
}, []);
// componentDidUpdate (特定状态 - count)
useEffect(() => {
if (isMounted.current) {
console.log('count 更新了:', count);
}
}, [count]);
// 通用的 componentDidUpdate
useEffect(() => {
if (isMounted.current) {
console.log('组件更新了');
}
}); // 注意:没有依赖数组
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
# react 调用 setState 之后发生了什么
- React 调用 setState 之后,React 会将新的 state 和 props 传递给组件的 render 方法,然后 React 会生成新的 DOM 元素。
- React 会将新的 DOM 元素渲染到 DOM 中
- React 会将新的 DOM 元素与旧的 DOM 元素进行比较,并生成一个更新计划
- React 会将更新计划应用到旧的 DOM 元素上,从而实现更新。
# useState 快照及累加问题
# useState 快照是什么?
在 React 中,状态变量在每次渲染时都是 固定的快照。设置状态会触发重新渲染,在新渲染中 React 会提供新的状态值。
// 不断进行累加会出现的问题
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => {
// 问题:使用当前渲染的快照值
setCount(count + 1); // count 是渲染时的快照
setCount(count + 1); // 这里 count 还是同一个值!
// 结果:只增加 1,而不是 2
};
// 正确的方式:使用函数更新
const handleIncrementCorrectly = () => {
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1);
// 结果:增加 2
};
// 在闭包中的问题
const handleAsyncIncrement = () => {
setTimeout(() => {
setCount(count + 1); // 这里使用的是创建闭包时的 count 值
}, 1000);
};
// 正确的异步更新
const handleAsyncIncrementCorrectly = () => {
setTimeout(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>错误累加(只+1)</button>
<button onClick={handleIncrementCorrectly}>正确累加(+2)</button>
<button onClick={handleAsyncIncrement}>错误异步</button>
<button onClick={handleAsyncIncrementCorrectly}>正确异步</button>
</div>
);
}
# useState 和 useReducer 的区别
# 基本概念对比
useState - 简单状态管理
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount((prev) => prev + 1)}>+1 (函数式)</button>
</div>
);
}
useReducer - 复杂状态管理
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'RESET':
return { ...state, count: 0 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
<button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
</div>
);
}
| 特性 | useState | useReducer |
|---|---|---|
| 适用场景 | 简单状态、独立状态 | 复杂状态、关联状态、复杂逻辑 |
| 状态结构 | 单个状态值 | 多个相关联的状态对象 |
| 更新逻辑 | 直接设置或简单转换 | 通过 action 和 reducer 处理 |
| 可预测性 | 较低 | 较高(纯函数) |
| 测试性 | 较难测试 | 容易测试(reducer 是纯函数) |
| 代码组织 | 逻辑分散在组件中 | 逻辑集中在 reducer 中 |
# useState/useReducer 深度嵌套更新问题
useState 的陷阱:
function UserProfile() {
const [user, setUser] = useState({
name: 'John',
profile: { age: 25, address: { city: 'Beijing' } },
});
// 错误:直接修改嵌套对象
const updateCity = (city: string) => {
user.profile.address.city = city; // ❌ 直接修改
setUser(user); // ❌ 不会触发重新渲染
};
// 正确:需要深度拷贝(代码冗长)
const updateCityCorrectly = (city: string) => {
setUser((prev) => ({
...prev,
profile: {
...prev.profile,
address: { ...prev.profile.address, city: city },
},
}));
};
}
useReducer 的解决方案:
type UserAction = { type: 'UPDATE_CITY'; payload: string } | { type: 'UPDATE_AGE'; payload: number };
function userReducer(state: UserState, action: UserAction): UserState {
switch (action.type) {
case 'UPDATE_CITY':
return {
...state,
profile: {
...state.profile,
address: { ...state.profile.address, city: action.payload },
},
};
default:
return state;
}
}
// 使用:代码更清晰
const updateCity = (city: string) => dispatch({ type: 'UPDATE_CITY', payload: city });
# useMemo / useCallback / useEffect 三者区别
这三个 Hook 解决的问题完全不同,可以用一句话区分:
| Hook | 解决什么问题 | 执行时机 | 一句话概括 |
|---|---|---|---|
| useEffect | 副作用处理 | 渲染后 | "渲染完做某事"(请求数据、修改 DOM) |
| useCallback | 函数引用稳定 | 渲染时 | "保持函数身份不变"(避免子组件重渲染) |
| useMemo | 计算结果缓存 | 渲染时 | "记住计算结果"(避免重复计算) |
# 1️⃣ useEffect - 处理副作用
作用:在组件渲染之后执行副作用操作(API 请求、订阅、手动 DOM 操作)
// 每次 count 变化后,打印日志
useEffect(() => {
console.log('count 变化了:', count);
}, [count]);
// 组件挂载时请求数据
useEffect(() => {
fetchData();
}, []); // 空数组 = 只执行一次
关键点:
- 在渲染之后异步执行
- 可以返回清理函数(用于取消订阅等)
- 不阻塞渲染
# 2️⃣ useCallback - 缓存函数引用
作用:保持函数引用稳定,防止因函数重建导致子组件不必要的重渲染
// ❌ 每次渲染都创建新函数,ChildComponent 会重渲染
const handleClick = () => {
setValue(value + 1);
};
// ✅ 函数引用不变,ChildComponent 不会重渲染(假设用了 React.memo)
const handleClick = useCallback(() => {
setValue(value + 1);
}, [value]); // value 不变,函数引用就不变
使用场景:传递给经过 React.memo 优化的子组件
function Parent() {
const [count, setCount] = useState(0);
// 不用 useCallback:Child 每次都重渲染
// 用 useCallback:只在 count 变化时重渲染
const handleClick = useCallback(() => {
console.log('点击了,当前值:', count);
}, [count]);
return <Child onClick={handleClick} />;
}
# 3️⃣ useMemo - 缓存计算结果
作用:缓存昂贵的计算结果,避免每次渲染都重新计算
// ❌ 每次渲染都重新计算(即使 list 没变)
const filteredData = dataList.filter((item) => item.price > 100);
// ✅ 只在 dataList 变化时重新计算
const filteredData = useMemo(() => {
console.log('执行过滤...');
return dataList.filter((item) => item.price > 100);
}, [dataList]);
使用场景:
- 昂贵的计算(排序、过滤、复杂运算)
- 保持引用稳定(避免子组件重渲染)
// 场景:避免因对象引用变化导致子组件重渲染
const style = useMemo(
() => ({
fontSize: '16px',
color: theme.color,
}),
[theme.color]
); // theme.color 不变,style 对象引用就不变
# 快速记忆
// 渲染后做事(异步)
useEffect(() => {
document.title = count;
}, [count]);
// 渲染时用(同步)
useCallback(() => {}, [deps]); // 缓存函数
useMemo(() => value, [deps]); // 缓存值
本质区别:
useEffect= 副作用(与渲染无关)useCallback= useMemo(fn, deps) 的语法糖(缓存函数)useMemo= 计算缓存(性能优化)
# Virtual DOM 和 Diff 算法
# 什么是 Virtual DOM?
Virtual DOM 是真实 DOM 的 JavaScript 对象表示,React 用它来提高渲染性能。
// 真实 DOM
<div class="container">
<h1>Hello</h1>
</div>
// Virtual DOM(JavaScript 对象)
{
type: 'div',
props: {
className: 'container',
children: {
type: 'h1',
props: {
children: 'Hello'
}
}
}
}
# Virtual DOM 的工作流程
- 创建:React 创建 Virtual DOM 树
- 对比:状态变化时,React 创建新的 Virtual DOM 树并与旧树对比(Diff 算法)
- 更新:计算出最小的差异,只更新需要变化的部分到真实 DOM
# React Diff 算法的三个策略
1. 只对同层级节点比较
// ❌ 不会跨层级比较
// 旧树
<div>
<A />
</div>
// 新树
<div>
<B />
<A />
</div>
// React 会销毁 A,创建 B,再创建 A(不会复用 A)
// ✅ 正确做法:使用 key 标识节点
2. 不同类型元素直接替换
// 旧
<div>Hello</div>
// 新
<span>Hello</span>
// React 会销毁 div 及其子节点,创建新的 span
3. 通过 key 识别节点
// ❌ 没有 key,React 按索引比较
// 旧:[A, B, C]
// 新:[A, C, B]
// React 会保留 A,更新 B→C,更新 C→B(浪费性能)
// ✅ 使用 key
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
// React 通过 key 精确找到对应节点,只移动位置
# 为什么 Virtual DOM 更快?
- 减少 DOM 操作:批量更新,避免频繁操作真实 DOM
- 跨浏览器兼容:抽象了浏览器差异
- 性能优化:只更新变化的部分
# React 中 key 的作用
# key 是什么?
key 是 React 用来识别列表项的唯一标识符。
// ❌ 使用索引(不推荐)
{
items.map((item, index) => <li key={index}>{item.name}</li>);
}
// ✅ 使用唯一 ID(推荐)
{
items.map((item) => <li key={item.id}>{item.name}</li>);
}
# key 的作用原理
React 使用 key 来:
- 识别节点:判断节点是新增、移动还是删除
- 复用节点:避免不必要的销毁和重建
- 保持状态:确保组件状态正确关联
// 示例:key 对组件状态的影响
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: '学习 React', checked: false },
{ id: 2, text: '写代码', checked: true },
]);
return (
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
# 常见错误
1. 使用索引作为 key
// ❌ 问题:列表顺序变化时,状态会错乱
{
items.map((item, index) => <TodoItem key={index} item={item} />);
}
// 场景:删除第一项,第二项的 key 从 1 变成 0
// React 会复用第一个组件,状态混乱
2. key 重复
// ❌ 错误:key 必须唯一
{
items.map((item) => (
<div key={item.type}>{item.name}</div>
// 如果多个 item 的 type 相同,key 就重复了
));
}
3. 随机生成 key
// ❌ 错误:每次渲染都变化
{
items.map((item) => (
<div key={Math.random()}>{item.name}</div>
// 每次都创建新节点,失去 key 的意义
));
}
# 正确使用
// ✅ 推荐:使用稳定的唯一 ID
{
users.map((user) => <UserCard key={user.id} user={user} />);
}
// ✅ 组合 key(没有唯一 ID 时)
{
items.map((item, index) => <div key={`${item.type}-${index}`}>{item.name}</div>);
}
# React 性能优化方法
# 1. 使用 React.memo 避免不必要的重渲染
// ❌ 每次父组件渲染,子组件都渲染
function Child({ name }) {
console.log('Child 渲染');
return <div>{name}</div>;
}
// ✅ 使用 React.memo 进行浅比较
const Child = React.memo(({ name }) => {
console.log('Child 渲染');
return <div>{name}</div>;
});
// 自定义比较函数
const Child = React.memo(
({ name }) => {
console.log('Child 渲染');
return <div>{name}</div>;
},
(prevProps, nextProps) => {
// 返回 true 表示不需要重渲染
return prevProps.name === nextProps.name;
}
);
# 2. 使用 useMemo 缓存计算结果
function ProductList({ products, filter }) {
// ❌ 每次渲染都重新计算
const filtered = products.filter((p) => p.category === filter);
// ✅ 只在 products 或 filter 变化时计算
const filtered = useMemo(() => {
return products.filter((p) => p.category === filter);
}, [products, filter]);
return <div>{filtered.map(/* ... */)}</div>;
}
# 3. 使用 useCallback 缓存函数
function Parent() {
const [count, setCount] = useState(0);
// ✅ 函数引用不变,Child 不会因父组件渲染而重渲染
const handleClick = useCallback(() => {
console.log('点击了');
}, []); // 空依赖,函数永远不变
return <Child onClick={handleClick} />;
}
const Child = React.memo(({ onClick }) => {
console.log('Child 渲染');
return <button onClick={onClick}>点击</button>;
});
# 4. 列表渲染使用 key
// ✅ 必须使用稳定的 key
{
items.map((item) => <div key={item.id}>{item.name}</div>);
}
# 5. 避免不必要的状态
// ❌ 冗余状态:可以从 props 派生
function User({ user }) {
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${user.firstName} ${user.lastName}`);
}, [user]);
return <div>{fullName}</div>;
}
// ✅ 直接计算
function User({ user }) {
const fullName = `${user.firstName} ${user.lastName}`;
return <div>{fullName}</div>;
}
# 6. 懒加载组件
import { lazy, Suspense } from 'react';
// ✅ 路由级别的代码分割
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
);
}
# 7. 虚拟滚动(长列表优化)
// 使用 react-window 或 react-virtualized
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
return (
<FixedSizeList height={400} itemCount={items.length} itemSize={50} width="100%">
{({ index, style }) => <div style={style}>{items[index].name}</div>}
</FixedSizeList>
);
}
# 性能优化总结
| 优化方法 | 使用场景 | 注意事项 |
|---|---|---|
| React.memo | 组件渲染昂贵,props 很少变化 | 只做浅比较,对象 props 要注意 |
| useMemo | 昂贵计算、保持引用稳定 | 不要过度使用,计算本身也有成本 |
| useCallback | 传递给优化过的子组件 | 配合 React.memo 使用 |
| 懒加载 | 路由、大组件 | 首屏加载性能提升明显 |
| 虚拟滚动 | 长列表(100+ 项) | 简单列表没必要 |
# 受控组件 vs 非受控组件
# 核心区别
| 特性 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据源 | React state | DOM |
| 值更新 | 通过 setState | 直接操作 DOM |
| 实时验证 | ✅ 支持 | ❌ 困难 |
| 使用场景 | 表单验证、动态表单 | 简单表单、集成第三方库 |
# 受控组件
表单元素的值由 React state 控制。
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(formData); // 直接使用 state
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name} // ✅ 受控:值来自 state
onChange={handleChange}
/>
<input
name="email"
value={formData.email} // ✅ 受控
onChange={handleChange}
/>
<textarea
name="message"
value={formData.message} // ✅ 受控
onChange={handleChange}
/>
<button type="submit">提交</button>
</form>
);
}
优点:
- ✅ 即时验证
- ✅ 条件禁用/启用输入
- ✅ 强制输入格式
// 实时验证示例
function PasswordInput() {
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
const value = e.target.value;
setPassword(value);
// 实时验证
if (value.length < 8) {
setError('密码至少8位');
} else {
setError('');
}
};
return (
<div>
<input type="password" value={password} onChange={handleChange} />
{error && <span className="error">{error}</span>}
</div>
);
}
# 非受控组件
表单元素的值由 DOM 自己管理,通过 ref 获取。
function ContactForm() {
const nameRef = useRef();
const emailRef = useRef();
const messageRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
// 通过 ref 获取值
console.log({
name: nameRef.current.value,
email: emailRef.current.value,
message: messageRef.current.value,
});
};
return (
<form onSubmit={handleSubmit}>
<input
ref={nameRef} // ✅ 非受控:通过 ref 操作
defaultValue="默认值" // 注意:用 defaultValue
/>
<input ref={emailRef} type="email" />
<textarea ref={messageRef} defaultValue="" />
<button type="submit">提交</button>
</form>
);
}
优点:
- ✅ 代码更简洁
- ✅ 更少的 state
- ✅ 集成第三方库(如日期选择器)
// 集成第三方库
function DatePicker() {
const dateRef = useRef();
const handleSubmit = () => {
// 直接使用第三方库的 API
const date = dateRef.current.getDate();
console.log(date);
};
return (
<div>
<SomeThirdPartyDatePicker ref={dateRef} />
<button onClick={handleSubmit}>提交</button>
</div>
);
}
# 如何选择?
// ✅ 使用受控组件的场景
- 需要实时验证
- 动态表单(字段数量变化)
- 需要条件禁用/启用
- 表单提交前需要所有数据
// ✅ 使用非受控组件的场景
- 简单表单(如登录框)
- 集成第三方库
- 不需要实时验证
- 性能敏感场景(大量表单项)
# React Context 的使用场景和原理
# 什么是 Context?
Context 提供了一种在组件树中跨层级传递数据的方式,避免了 props 逐层传递(props drilling)。
// ❌ Props Drilling:逐层传递
function App() {
const user = { name: 'John' };
return <Header user={user} />;
}
function Header({ user }) {
return <Navbar user={user} />;
}
function Navbar({ user }) {
return <UserMenu user={user} />;
}
function UserMenu({ user }) {
return <span>{user.name}</span>; // 终于用到了
}
// ✅ 使用 Context
const UserContext = createContext();
function App() {
const user = { name: 'John' };
return (
<UserContext.Provider value={user}>
<Header />
</UserContext.Provider>
);
}
function UserMenu() {
const user = useContext(UserContext);
return <span>{user.name}</span>; // 直接获取
}
# Context 的基本使用
1. 创建 Context
import { createContext, useContext } from 'react';
// 创建 Context
const ThemeContext = createContext();
export default ThemeContext;
2. 提供 Context
import ThemeContext from './ThemeContext';
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
<Footer />
</ThemeContext.Provider>
);
}
3. 消费 Context
import { useContext } from 'react';
import ThemeContext from './ThemeContext';
function Header() {
// 方式1:使用 useContext Hook
const { theme, setTheme } = useContext(ThemeContext);
return (
<header className={theme}>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>切换主题</button>
</header>
);
}
// 方式2:使用 Consumer(类组件)
class Footer extends React.Component {
render() {
return <ThemeContext.Consumer>{({ theme }) => <footer className={theme}>Footer</footer>}</ThemeContext.Consumer>;
}
}
# Context 的常见使用场景
1. 主题切换
const ThemeContext = createContext();
function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<Page />
</ThemeContext.Provider>
);
}
function Button() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme} style={{ background: theme === 'light' ? '#fff' : '#333' }}>
切换主题
</button>
);
}
2. 用户信息
const AuthContext = createContext();
function App() {
const [user, setUser] = useState(null);
return <AuthContext.Provider value={{ user, setUser }}>{user ? <Dashboard /> : <Login />}</AuthContext.Provider>;
}
function Login() {
const { setUser } = useContext(AuthContext);
return <button onClick={() => setUser({ name: 'John' })}>登录</button>;
}
3. 国际化(i18n)
const I18nContext = createContext();
const translations = {
en: { welcome: 'Welcome' },
zh: { welcome: '欢迎' },
};
function App() {
const [lang, setLang] = useState('zh');
return (
<I18nContext.Provider value={{ lang, translations }}>
<Page />
</I18nContext.Provider>
);
}
# Context 的注意事项
1. Context 更新会导致所有消费者重渲染
// ❌ 问题:频繁更新导致性能问题
function App() {
const [data, setData] = useState({});
const [count, setCount] = useState(0);
return (
<DataContext.Provider value={{ data, setData }}>
<CountContext.Provider value={{ count, setCount }}>
<Child />
</CountContext.Provider>
</DataContext.Provider>
);
}
// ✅ 解决:拆分 Context,按需更新
const DataContext = createContext();
const CountContext = createContext();
2. 默认值只在没有 Provider 时生效
const ThemeContext = createContext('light'); // 默认值
function App() {
// 没有 Provider,使用默认值
return <Header />;
}
function Header() {
const theme = useContext(ThemeContext); // 'light'
return <div className={theme}>Header</div>;
}
3. 性能优化
// ❌ 每次都创建新对象,导致消费者重渲染
<ThemeContext.Provider value={{ theme, setTheme }}>
// ✅ 使用 useMemo 稳定引用
const value = useMemo(() => ({ theme, setTheme }), [theme]);
<ThemeContext.Provider value={value}>
# Context vs Redux
| 特性 | Context | Redux |
|---|---|---|
| 复杂度 | 简单 | 较复杂 |
| 学习曲线 | 低 | 高 |
| 状态管理 | 基础 | 强大(中间件、时间旅行) |
| 性能 | 可能重渲染较多 | 细粒度控制 |
| 适用场景 | 小型应用、主题、用户信息 | 大型应用、复杂状态逻辑 |
选择建议:
- 🎯 简单的全局配置(主题、语言) → Context
- 🎯 复杂的状态管理(多个状态、异步逻辑) → Redux / Zustand
# Redux/Mobx 的核心概念
Redux 和 MobX 是 React 生态中最流行的状态管理库,它们采用了不同的状态管理哲学
# Redux 核心概念
Redux 是一个可预测的状态容器,采用 单向数据流 和 不可变数据。
# 三大原则
- 单一数据源:整个应用的 state 存储在单一 store 的对象树中
- State 只读:修改 state 的唯一方式是触发 action
- 使用纯函数执行修改:通过 reducer 纯函数计算新 state
# 核心概念
// 1. Action - 描述发生了什么
const ADD_TODO = 'ADD_TODO';
const addTodo = (text) => ({
type: ADD_TODO,
payload: text,
});
// 2. Reducer - 纯函数,根据 action 计算新 state
const todosReducer = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
// ✅ 不可变更新:返回新对象
return [...state, { id: Date.now(), text: action.payload, completed: false }];
case 'TOGGLE_TODO':
return state.map((todo) => (todo.id === action.id ? { ...todo, completed: !todo.completed } : todo));
default:
return state;
}
};
// 3. Store - 存储 state
import { createStore } from 'redux';
const store = createStore(todosReducer);
// 4. 使用
console.log(store.getState()); // 获取 state
store.dispatch(addTodo('学习 Redux')); // 触发 action
store.subscribe(() => console.log(store.getState())); // 监听变化
# Redux 数据流
┌─────────┐ ┌──────────┐ ┌─────────┐
│ View │────▶│ Action │────▶│ Store │
└─────────┘ └──────────┘ └─────────┘
▲ │
└───────── Reducer ◀───────────────┘
- View 触发 Action
- Store 接收 Action,交给 Reducer
- Reducer 返回新 State
- Store 通知 View 更新
# Redux Toolkit(现代 Redux)
Redux Toolkit(RTK)是官方推荐的 Redux 编写方式,简化了配置。
import { createSlice, configureStore } from '@reduxjs/toolkit';
// 1. 创建 slice(包含 reducer + actions)
const todoSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: (state, action) => {
// ✅ Immer 允许直接修改
state.push({ id: Date.now(), text: action.payload, completed: false });
},
toggleTodo: (state, action) => {
const todo = state.find((t) => t.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
},
});
// 2. 提取 actions
const { addTodo, toggleTodo } = todoSlice.actions;
// 3. 创建 store
const store = configureStore({
reducer: {
todos: todoSlice.reducer,
},
});
// 4. 使用
store.dispatch(addTodo('学习 RTK'));
# Redux 异步处理(Redux Thunk)
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// 创建异步 thunk
const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await fetch('/api/todos');
return response.json();
});
const todoSlice = createSlice({
name: 'todos',
initialState: { items: [], status: 'idle', error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
},
});
// 在组件中使用
function TodoList() {
const dispatch = useDispatch();
const { items, status } = useSelector((state) => state.todos);
useEffect(() => {
dispatch(fetchTodos());
}, [dispatch]);
if (status === 'loading') return <div>加载中...</div>;
return (
<ul>
{items.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
# MobX 核心
MobX 采用 响应式编程 和 可变状态,通过装饰器或函数自动追踪依赖。
# 核心概念
- Observable State:可观察的状态
- Computed Values:派生值(自动更新)
- Actions:修改状态的方法
- Reactions:响应状态变化
// 1. 创建 Store
import { makeAutoObservable } from 'mobx';
class TodoStore {
todos = [];
constructor() {
makeAutoObservable(this);
}
addTodo(text) {
this.todos.push({ id: Date.now(), text });
}
}
const todoStore = new TodoStore();
export default todoStore;
// 2. React 组件
import React from 'react';
import { observer } from 'mobx-react';
import todoStore from './store';
const App = observer(() => {
const handleSubmit = (e) => {
e.preventDefault();
const input = e.target.elements.todo;
todoStore.addTodo(input.value);
input.value = '';
};
return (
<div>
<h1>MobX 待办</h1>
<form onSubmit={handleSubmit}>
<input name="todo" placeholder="添加任务" />
<button type="submit">添加</button>
</form>
<ul>
{todoStore.todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
});
export default App;
# Redux vs MobX 对比
| 特性 | Redux | MobX |
|---|---|---|
| 理念 | 单向数据流、函数式编程 | 响应式编程、面向对象 |
| 状态可变性 | 不可变(immutable) | 可变(mutable) |
| 代码风格 | 冗长但规范 | 简洁灵活 |
| 学习曲线 | 陡峭 | 平缓 |
| 性能 | 需要手动优化 | 自动优化 |
| 调试工具 | 完善(Redux DevTools) | 相对简单 |
| 类型支持 | TypeScript 支持好 | 需要额外配置 |
| 样板代码 | 多 | 少 |
# 代码对比
// ========== Redux ==========
// action types
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
// action creators
const addTodo = (text) => ({ type: ADD_TODO, payload: text });
const toggleTodo = (id) => ({ type: TOGGLE_TODO, payload: id });
// reducer
function todosReducer(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, { id: Date.now(), text: action.payload, completed: false }];
case TOGGLE_TODO:
return state.map((todo) => (todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo));
default:
return state;
}
}
// store
const store = createStore(todosReducer);
// 组件中使用
function TodoList() {
const todos = useSelector((state) => state.todos);
const dispatch = useDispatch();
return (
<>
{todos.map((todo) => (
<div key={todo.id}>
<span>{todo.text}</span>
<button onClick={() => dispatch(toggleTodo(todo.id))}>Toggle</button>
</div>
))}
<button onClick={() => dispatch(addTodo('New Todo'))}>Add</button>
</>
);
}
// ========== MobX ==========
class TodoStore {
todos = observable([]);
addTodo = action((text) => {
this.todos.push({ id: Date.now(), text, completed: false });
});
toggleTodo = action((id) => {
const todo = this.todos.find((t) => t.id === id);
if (todo) todo.completed = !todo.completed;
});
}
// 组件中使用
const TodoList = observer(({ store }) => {
return (
<>
{store.todos.map((todo) => (
<div key={todo.id}>
<span>{todo.text}</span>
<button onClick={() => store.toggleTodo(todo.id)}>Toggle</button>
</div>
))}
<button onClick={() => store.addTodo('New Todo')}>Add</button>
</>
);
});
# 如何选择?
选择 Redux 的场景:
✅ 大型应用,复杂的状态逻辑
✅ 需要可预测的状态变化
✅ 团队协作,需要规范化
✅ 需要时间旅行调试
✅ 状态变化需要记录和追溯
// Redux 典型场景:电商应用
const store = {
cart: {
items: [],
total: 0,
discount: 0,
},
products: {
list: [],
filters: { category: null, priceRange: null },
sortBy: 'price',
},
user: {
profile: null,
orders: [],
preferences: {},
},
};
选择 MobX 的场景:
✅ 中小型应用
✅ 快速开发原型
✅ 团队更熟悉面向对象
✅ 不想写太多样板代码
✅ 性能敏感场景
// MobX 典型场景:表单应用
class FormStore {
formData = observable({
username: '',
email: '',
password: '',
});
errors = observable({});
get isValid() {
return this.formData.username && this.formData.email && this.formData.password;
}
validate = action(() => {
this.errors = {};
if (!this.formData.username) this.errors.username = '必填';
if (!this.formData.email) this.errors.email = '必填';
});
}
选择 Zustand(新兴方案):
Zustand 是一个更轻量、更简单的状态管理库。
import { create } from 'zustand';
const useStore = create((set) => ({
todos: [],
addTodo: (text) =>
set((state) => ({
todos: [...state.todos, { id: Date.now(), text, completed: false }],
})),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map((todo) => (todo.id === id ? { ...todo, completed: !todo.completed } : todo)),
})),
}));
// 组件中使用
function TodoList() {
const { todos, addTodo, toggleTodo } = useStore();
return (
<>
{todos.map((todo) => (
<div key={todo.id}>
<span>{todo.text}</span>
<button onClick={() => toggleTodo(todo.id)}>Toggle</button>
</div>
))}
<button onClick={() => addTodo('New')}>Add</button>
</>
);
}
现代状态管理推荐
- 大型项目:Redux Toolkit(官方推荐)
- 中小型项目:Zustand(简洁、轻量)
- 快速原型:MobX(灵活、简单)
- 简单全局状态:React Context(内置方案)
# ref 是什么?为什么需要 ref?
ref 是 React 提供的一种访问 DOM 元素或组件实例的方式。
React 是数据驱动的,通常通过 state 和 props 来控制 UI。但有些场景需要直接访问 DOM,比如:聚焦输入框、播放视频、测量元素尺寸等。这时就需要 ref。
# ref 的三种创建方式
1. useRef Hook(推荐)
import { useRef } from 'react';
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
// 直接操作 DOM
inputRef.current?.focus();
};
return (
<div>
<input ref={inputRef} placeholder="点击按钮聚焦" />
<button onClick={focusInput}>聚焦</button>
</div>
);
}
2. createRef(类组件)
class Form extends React.Component {
inputRef = React.createRef<HTMLInputElement>();
focusInput = () => {
this.inputRef.current?.focus();
};
render() {
return (
<div>
<input ref={this.inputRef} />
<button onClick={this.focusInput}>聚焦</button>
</div>
);
}
}
3. 回调 ref
function Form() {
let inputRef: HTMLInputElement | null = null;
const focusInput = () => {
inputRef?.focus();
};
return (
<div>
<input ref={(node) => (inputRef = node)} />
<button onClick={focusInput}>聚焦</button>
</div>
);
}
# ref 的常见使用场景
function Examples() {
const inputRef = useRef<HTMLInputElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const divRef = useRef<HTMLDivElement>(null);
// 1️⃣ 聚焦输入框
const focus = () => inputRef.current?.focus();
// 2️⃣ 控制视频播放
const playVideo = () => videoRef.current?.play();
const pauseVideo = () => videoRef.current?.pause();
// 3️⃣ 获取元素尺寸
const getSize = () => {
const width = divRef.current?.offsetWidth;
const height = divRef.current?.offsetHeight;
console.log({ width, height });
};
// 4️⃣ 滚动到元素
const scrollTo = () => {
divRef.current?.scrollIntoView({ behavior: 'smooth' });
};
return (
<div>
<input ref={inputRef} />
<button onClick={focus}>聚焦</button>
<video ref={videoRef} src="video.mp4" />
<button onClick={playVideo}>播放</button>
<div ref={divRef} style={{ height: '200px', overflow: 'auto' }}>
内容区域
</div>
<button onClick={scrollTo}>滚动到位置</button>
</div>
);
}
# ref VS state
| 特性 | ref | state |
|---|---|---|
| 更新 | ref.current = xxx(立即) | setState()(异步重渲染) |
| 读取 | ref.current | 直接读取 state |
| 触发渲染 | ❌ 不会触发 | ✅ 会触发 |
| 用途 | 存储不触发渲染的数据 | 存储触发渲染的 UI 数据 |
function Counter() {
const [count, setCount] = useState(0); // 会触发渲染
const renderCount = useRef(0); // 不会触发渲染
// ❌ 错误:用 ref 存储会导致不同步
const handleClick = () => {
renderCount.current += 1; // 不会触发重渲染,UI 不更新
};
// ✅ 正确:用 state 存储需要渲染的数据
const handleClick = () => {
setCount(count + 1); // 会触发重渲染,UI 更新
};
// ✅ ref 适合存储不需要渲染的数据
const logRenderCount = () => {
renderCount.current += 1;
console.log('渲染次数:', renderCount.current);
};
return <button onClick={handleClick}>点击</button>;
}
# ref 的注意事项
// ❌ 错误:不要在渲染期间读取/写入 ref
function Component() {
const ref = useRef(0);
// 错误:渲染期间修改 ref
ref.current = ref.current + 1;
// ✅ 正确:在事件或 useEffect 中修改
useEffect(() => {
ref.current = ref.current + 1;
}, []);
return <div>...</div>;
}
// ❌ 错误:ref 只能绑定到 DOM 元素
function Parent() {
const ref = useRef(null);
// 如果 Child 是函数组件,ref 无法传递
return <Child ref={ref} />;
}
// ✅ 正确:使用 forwardRef
const Child = React.forwardRef((props, ref) => {
return <input ref={ref} />;
});
# React.forwardRef 是什么
forwardRef 是 React 提供的高阶组件,用于将 ref 从父组件传递到子组件的 DOM 元素。
为什么需要 forwardRef?
默认情况下,React 组件无法直接接收 ref(ref 只能绑定到 DOM 元素或类组件实例)。forwardRef 解决了这个问题。
# 基本用法
// ❌ 错误:ref 无法传递
const MyInput = ({ value }: { value: string }) => {
return <input value={value} />;
};
// ✅ 正确:使用 forwardRef
const MyInput = React.forwardRef<HTMLInputElement, { value: string }>((props, ref) => {
return <input ref={ref} value={props.value} />;
});
// 使用
const inputRef = useRef<HTMLInputElement>(null);
function App() {
const handleClick = () => {
inputRef.current?.focus(); // 可以访问 DOM
};
return (
<div>
<MyInput ref={inputRef} value="Hello" />
<button onClick={handleClick}>聚焦</button>
</div>
);
}
# forwardRef + useImperativeHandle
将自定义方法暴露给父组件:
const MyInput = React.forwardRef<{ focus: () => void; reset: () => void }, { value: string }>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
reset: () => {
if (inputRef.current) {
inputRef.current.value = '';
}
},
}));
return <input ref={inputRef} value={props.value} />;
});
// 父组件
function App() {
const inputRef = useRef<{ focus: () => void; reset: () => void }>(null);
return (
<>
<MyInput ref={inputRef} value="Hello" />
<button onClick={() => inputRef.current?.focus()}>聚焦</button>
<button onClick={() => inputRef.current?.reset()}>重置</button>
</>
);
}
# 常见使用场景
| 场景 | 示例 |
|---|---|
| 表单组件 | Input、Select 组件 |
| UI 组件库 | Button、Modal 组件 |
| 动画库 | 暴露 play/pause 方法 |
| 第三方封装 | 封装原生 DOM 元素 |
# 注意事项
// ❌ 避免:过度使用 forwardRef
const Button = forwardRef<HTMLButtonElement>((props, ref) => {
return <button ref={ref}>点击</button>;
});
// ✅ 推荐:仅在需要访问 DOM 时使用
const Button = ({ onClick }: { onClick: () => void }) => {
return <button onClick={onClick}>点击</button>;
};