尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

五、React状态变量

五、React状态变量 一、状态变量的定义在 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。点其中一个绝对不会影响另外两个。它们就像三个独立的小房间互不干扰。四、useReduceruseReducer是 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 })} 触发改变 /button4. 例子用户个人资料编辑表单假设你在开发一个“个人资料”页面用户需要修改昵称、年龄和邮箱。如果用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. 对比对比维度useStateuseReducer代码组织逻辑分散在组件的各个事件处理函数中逻辑集中在独立的 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; }
返回列表