一、状态变量的定义
在 React 中,状态变量(State)就像是组件的“记忆”或“内存”,专门用来存储组件内部那些会动态变化的数据。
- 普通变量:值改变后,页面不会有任何反应。
- 状态变量:值一旦改变,React 就会立刻感知到,并自动重新渲染页面,把最新的数据显示给用户。
| 对比项 | 普通变量 | 状态变量 |
|---|---|---|
| 值改变时 | 页面无任何反应 | React 自动重新渲染页面 |
| 典型用途 | 存储不影响 UI 的临时数据 | 存储需要实时展示的动态数据 |
| 修改方式 | 直接赋值 | 通过更新函数 |
一句话总结:状态变了 ➡️ 页面自动更新。
二、状态变量使用方法(useState)
第一步:导入 useState
在组件的最顶部,从 react 中引入它。
import { useState } from 'react';第二步:调用 useState 声明状态
使用数组解构的方式,接收两个值:当前的状态值 和 修改状态的函数。
const [状态变量名, 更新函数名] = useState(初始值);- 命名规范:更新函数通常以
set开头,比如count对应setCount,这样代码可读性最强。 - 初始值:可以是数字、字符串、布尔值,甚至是对象或数组。
第三步:在页面中使用和修改状态
- 展示:直接用大括号
{状态变量名}渲染在页面上。 - 修改:必须通过“更新函数”来修改,绝对不能直接赋值(如
count = count + 1是错误的写法)。
例子:电商购物车的“加入购物车”按钮
假设你正在做一个电商网站,商品详情页有一个按钮,点击后提示用户已加入几件商品:
import { useState } from 'react'; function ProductPage() { // 1. 初始化状态:购物车数量初始为 0 const [cartCount, setCartCount] = useState(0); // 2. 定义点击事件处理函数 const handleAddToCart = () => { // 3. 通过更新函数修改状态 setCartCount(cartCount + 1); }; return ( <div> <h2>最新款运动鞋</h2> <p>价格:¥599</p> {/* 4. 在页面中展示状态变量 */} <button onClick={handleAddToCart}> 加入购物车 (当前已选: {cartCount} 件) </button> </div> ); }三、状态变量的三大“铁律”(useState)
铁律一:状态更新是“异步”的(画师需要时间)
当你调用setLikeCount时,likeCount不会在下一行立刻变成新值。React 会把更新攒一攒,然后统一刷新页面。
实际场景:别在
setCount(count + 1)下面紧接着写console.log(count),打印出来的还是旧值。
铁律二:状态更新是“替换”而不是“合并”
如果你有一个状态是个对象(比如用户信息),你想改其中一个属性,必须把整个对象重新传进去。
const [user, setUser] = useState({ name: '小明', age: 18 }); // ❌ 错误写法:age 会消失,只剩下 role setUser({ role: '前端新手' }); // ✅ 正确写法:用展开运算符(...)把旧属性带上,再覆盖新属性 setUser({ ...user, role: '前端新手' });铁律三:状态的作用域是“隔离”的
如果你在页面上放了 3 个<LikeButton />,它们各自有各自的likeCount。点其中一个,绝对不会影响另外两个。它们就像三个独立的小房间,互不干扰。
四、useReducer
useReducer是 React 提供的一个 Hook(钩子函数),专门用来集中管理和更新复杂的状态。
useReducer强迫你把所有的修改逻辑(比如增加、删除、修改)都写在一个独立的函数里。当组件需要改变状态时,只需要向这个调度中心发送一个“指令(Action)”,调度中心就会根据指令计算出新的状态,并触发页面更新。
核心优势:将“UI 渲染”和“状态修改逻辑”彻底分离,让代码更清晰、更容易测试和维护。
五、useReducer 的使用方法
使用useReducer分为标准的三步:
1. 编写 Reducer 函数(规则函数)
这是一个纯函数,专门负责处理状态的变化。它接收两个参数:
state:当前的状态。action:一个包含type属性的普通对象,代表你要执行的动作。
// 命名规范:xxxReducer function myReducer(state, action) { switch (action.type) { case '动作类型A': // 返回新的状态(注意:不能直接修改原 state,必须返回新对象) return { ...state, 属性: '新值' }; case '动作类型B': return { ...state, 属性: '另一个新值' }; default: // 如果是不认识的动作,必须返回原状态 return state; } }2. 在组件中调用 useReducer
在组件内部调用它,它会返回一个数组,包含当前状态和派发函数(dispatch)。
// 语法:const [当前状态, 派发函数] = useReducer(reducer函数, 初始状态); const [state, dispatch] = useReducer(myReducer, { count: 0 });3. 通过 dispatch 触发状态更新
在事件处理函数中,调用dispatch并传入一个动作对象。
<button onClick={() => dispatch({ type: '动作类型A' })}> 触发改变 </button>4. 例子:用户个人资料编辑表单
假设你在开发一个“个人资料”页面,用户需要修改昵称、年龄和邮箱。如果用useState,你需要写三个状态和三个修改函数。用useReducer会非常优雅:
import { useReducer } from 'react'; // 1. 初始状态 const initialState = { nickname: '', age: '', email: '' }; // 2. Reducer 函数(集中管理所有修改逻辑) function profileReducer(state, action) { switch (action.type) { case 'UPDATE_NICKNAME': return { ...state, nickname: action.payload }; // payload 是传递的额外数据 case 'UPDATE_AGE': return { ...state, age: action.payload }; case 'UPDATE_EMAIL': return { ...state, email: action.payload }; case 'RESET': return initialState; // 重置为初始状态 default: return state; } } function ProfileForm() { // 3. 使用 useReducer const [state, dispatch] = useReducer(profileReducer, initialState); return ( <div> <input placeholder="昵称" value={state.nickname} onChange={(e) => dispatch({ type: 'UPDATE_NICKNAME', payload: e.target.value })} /> <input placeholder="年龄" value={state.age} onChange={(e) => dispatch({ type: 'UPDATE_AGE', payload: e.target.value })} /> <input placeholder="邮箱" value={state.email} onChange={(e) => dispatch({ type: 'UPDATE_EMAIL', payload: e.target.value })} /> <button onClick={() => dispatch({ type: 'RESET' })}>重置表单</button> <p>当前填写的内容:{JSON.stringify(state)}</p> </div> ); }六、选择指南
1. 什么时候果断选 useState?
状态结构简单且独立:状态是基础类型(数字、字符串、布尔值),且各个状态之间“互不干涉”。比如控制弹窗显隐的
isOpen、简单的计数器count。更新逻辑非常直接:修改状态只需要简单的赋值或计算,不需要复杂的条件判断。比如
setCount(count + 1)。代码量越少越好:对于简单的交互,
useState的代码体积更小,上手成本最低,可读性也更好。
2. 什么时候必须上 useReducer?
多个状态相互关联(联动):如果修改一个状态,往往需要连带修改其他几个状态。比如“购物车”场景,你添加一件商品(修改商品列表),必须同时更新总数量和总金额。用
useReducer可以在一个动作里一次性更新所有相关状态,避免数据不同步。状态更新逻辑复杂:如果状态的改变需要经过复杂的条件判断(比如表单校验:密码和确认密码是否一致、是否为空等)。把这些判断逻辑集中写在
reducer里,能让组件本身保持干净。状态机(步骤流):当组件有明确的状态流转时,比如文件上传(空闲 ➡️ 上传中 ➡️ 成功/失败),用
useReducer配合switch-case会比用多个布尔值(isUploading、isSuccess、isError)清晰得多。需要复用逻辑或方便调试:
reducer是一个纯函数,你可以把它提取到单独的文件中,供多个组件复用。同时,由于所有的状态变更都通过dispatch触发,排查 Bug 时你可以轻松追踪到是哪个动作引发了状态改变。
3. 对比
| 对比维度 | useState | useReducer |
|---|---|---|
| 代码组织 | 逻辑分散在组件的各个事件处理函数中 | 逻辑集中在独立的 reducer 函数中 |
| 更新方式 | 直接调用 setter 函数赋值 | 派发 action(动作),由 reducer 计算新状态 |
| 适用场景 | 简单状态、独立状态 | 复杂对象、状态联动、状态机 |
| 可测试性 | 测试较难,因为逻辑和 UI 耦合在一起 | 极易测试,可单独对 reducer 函数进行单元测试 |
七、useState 状态变量修改
在 React 中,绝对不能直接修改状态变量(比如直接写count = 10),而是必须使用useState提供给你的更新函数(通常命名为set + 状态名)来修改它。
1. 基础类型(数字、字符串、布尔值)
对于简单的数据类型,直接在更新函数里传入新值即可。
实际场景:一个控制弹窗显示/隐藏的开关。
const [isModalShow, setIsModalShow] = useState(false); // 初始为隐藏 // ✅ 正确做法:使用更新函数 setIsModalShow(true); // ❌ 错误做法:直接修改 isModalShow = true; // 页面不会有任何反应!2. 对象类型(⚠️ 新手最容易踩坑的地方)
在修改对象时,必须创建一个全新的对象,而不能在原来的对象上直接修改属性。
实际场景:修改用户的年龄信息。
const [user, setUser] = useState({ name: '张三', age: 20 }); // ❌ 错误做法:直接修改原对象(React 发现内存地址没变,不会刷新页面) user.age = 21; setUser(user); // ✅ 正确做法:利用展开运算符(...)复制一份旧数据,再修改新数据 setUser(prevUser => ({ ...prevUser, // 把旧属性都复制过来 age: 21 // 只修改需要变的部分 }));3. 数组类型
和对象一样,修改数组时也不能直接对原数组进行增删改(比如不能用push、pop),而是要返回一个全新的数组。
实际场景:往待办事项列表里添加一个新任务。
const [todos, setTodos] = useState(['学习React']); // ❌ 错误做法:直接 push todos.push('吃晚饭'); setTodos(todos); // 页面不会更新! // ✅ 正确做法:利用展开运算符(...)生成新数组 setTodos(prevTodos => [...prevTodos, '吃晚饭']);八、useReducer状态变量修改
在useReducer中,你同样不能直接修改状态变量。它的修改方式比useState更有仪式感,你需要通过“派发(dispatch)一个动作(action)”来告诉 React 你要怎么改。
1. 基础用法:派发一个动作
实际场景:一个电商购物车,点击按钮增加商品数量。
// 1. 定义机械臂(reducer函数):它接收旧状态和动作,返回新状态 function cartReducer(state, action) { // 根据动作的类型(type)来决定怎么改状态 if (action.type === 'ADD_ITEM') { return { ...state, count: state.count + 1 }; // 必须返回新对象! } return state; // 如果是不认识的动作,原样返回 } // 2. 在组件中使用 const [state, dispatch] = useReducer(cartReducer, { count: 0 }); // ✅ 正确做法:通过 dispatch 派发一个动作 dispatch({ type: 'ADD_ITEM' }); // ❌ 错误做法:直接修改 state.count = 1; // 页面不会更新!2. 进阶用法:动作里携带参数(payload)
实际场景:在搜索框输入内容,或者点击删除指定 ID 的购物车商品。这时候光说“我要修改”不够,还得告诉 reducer“修改成什么”。
function cartReducer(state, action) { switch (action.type) { case 'REMOVE_ITEM': // action.payload 就是传递过来的额外数据(比如商品ID) return { ...state, items: state.items.filter(item => item.id !== action.payload) }; default: return state; } } // ✅ 正确做法:派发带有 payload 的动作 dispatch({ type: 'REMOVE_ITEM', payload: '商品ID_123' });九、状态变量修改避坑指南
🚨 坑点一:异步更新与“闭包陷阱”(连续更新失效)
- 现象:在同一个点击事件里连续调用多次
setState,发现只增加了一次。 - 原因:React 为了性能优化,会把短时间内的多次状态更新合并成一次处理(批量更新)。而且
setState是异步的,直接传值时,你拿到的都是当前渲染周期里的“旧值”。 - 实际场景:点击一次按钮,想让计数器加 3。
const [count, setCount] = useState(0); const addThree = () => { // ❌ 错误:连续三次拿的都是旧值 0,最终结果只会变成 1 setCount(count + 1); setCount(count + 1); setCount(count + 1); }; // ✅ 正确:使用“函数式更新”,让 React 基于最新的状态去计算 const addThree = () => { setCount(prev => prev + 1); // 0 -> 1 setCount(prev => prev + 1); // 1 -> 2 setCount(prev => prev + 1); // 2 -> 3 };🚨 坑点二:更新后立刻获取新值(拿到旧值)
- 现象:刚调用完
setState,紧接着console.log,发现打印的还是旧值。 - 原因:状态更新是异步调度的,不会立即生效。
- 实际场景:点击按钮后打印最新状态。
const handleClick = () => { setCount(5); console.log(count); // ❌ 打印的还是旧值! }; // ✅ 正确:如果你想获取更新后的值,请使用 useEffect 监听 useEffect(() => { console.log('更新后的 count:', count); // 状态真正更新后会执行 }, [count]);🚨 坑点三:直接修改对象/数组(原地修改)
- 现象:明明调用了
setState,但页面毫无反应。 - 原因:React 是通过“浅比较”(比较内存引用地址)来判断状态是否变化的。如果你直接修改了原对象或数组,内存地址没变,React 会以为状态没变,从而拒绝重新渲染。
- 实际场景:给购物车添加商品。
const [cart, setCart] = useState({ items: ['苹果'] }); const addItem = () => { // ❌ 错误:直接修改原数组(原地修改) cart.items.push('香蕉'); setCart(cart); // 引用没变,页面不更新! }; // ✅ 正确:必须创建全新的对象/数组 const addItem = () => { setCart(prevCart => ({ ...prevCart, items: [...prevCart.items, '香蕉'] // 返回新数组 })); };🚨 坑点四:初始值计算太耗时(性能浪费)
- 现象:每次组件重新渲染时,页面都会卡顿一下。
- 原因:如果你直接把一个复杂的计算函数作为
useState的初始值,每次组件渲染都会重新执行这个函数,非常浪费性能。 - 实际场景:从本地存储读取大量数据作为初始状态。
// ❌ 错误:每次渲染都会执行 heavyComputation() const [data, setData] = useState(heavyComputation()); // ✅ 正确:使用“惰性初始化”,传入一个函数,它只在组件首次挂载时执行一次 const [data, setData] = useState(() => { return heavyComputation(); // 仅首次渲染执行,后续渲染直接跳过 });(注:useReducer也有同样的惰性初始化机制,可以传入第三个参数init函数来优化性能
十、惰性初始化
惰性初始化的核心思想:把“计算初始值”的过程,变成一个函数传给 Hook,而不是直接把计算结果传进去。
为什么要这么做?因为在 React 中,组件每次重新渲染时,整个函数体都会重新执行。如果不加函数包裹,耗时的计算就会在每次渲染时都跑一遍,白白浪费性能。
1. useState 的惰性初始化
实际场景:页面加载时,需要从本地存储读取用户的个性化配置(比如主题颜色、字体大小)。解析 JSON 数据需要一点点时间。
// 模拟一个耗时的读取函数 function getUserConfig() { console.log('正在读取用户配置...'); // 每次渲染都会打印,性能浪费! const config = localStorage.getItem('userConfig'); return config ? JSON.parse(config) : { theme: 'light', fontSize: 14 }; } function App() { // ❌ 错误写法:直接传入函数执行的结果 // 每次组件渲染,getUserConfig() 都会被执行一次 // const [config, setConfig] = useState(getUserConfig()); // ✅ 正确写法:传入函数本身(注意没有括号) // getUserConfig 只有在组件第一次挂载时才会执行 const [config, setConfig] = useState(getUserConfig); return <div>当前主题:{config.theme}</div>; }2. useReducer 的惰性初始化
实际场景:假设我们的用户配置比较复杂,不仅有主题和字体,还有“历史记录”。我们需要在初始化时,不仅读取配置,还要根据配置生成初始的历史记录。这时候useReducer的第三个参数init就派上用场了。
// 1. 定义初始化函数(接收一个参数 arg,返回初始状态) function initConfig(defaultTheme) { console.log('正在初始化复杂配置...'); const config = localStorage.getItem('userConfig'); const parsed = config ? JSON.parse(config) : { theme: defaultTheme, fontSize: 14 }; // 可以在这里做更多复杂的初始化逻辑 return { ...parsed, history: [parsed.theme] // 比如:把初始主题加入历史记录 }; } // 2. 定义 reducer function configReducer(state, action) { switch (action.type) { case 'SET_THEME': return { ...state, theme: action.payload, history: [...state.history, action.payload] }; default: return state; } } function App() { // ✅ 正确写法:使用第三个参数 init // 1. configReducer: 状态更新规则 // 2. 'dark': 传给 initConfig 的参数(initialArg) // 3. initConfig: 初始化函数 const [config, dispatch] = useReducer(configReducer, 'dark', initConfig); return <div>当前主题:{config.theme}</div>; }