文章目录一、使用 State 响应输入1. 命令式 vs 声明式2. 声明式地设计 UI 的 5 个步骤第一步定位组件中不同的视图状态第二步确定触发状态改变的因素第三步用 useState 表示内存中的 State第四步删除不必要的 State精简逻辑第五步连接事件处理函数二、选择状态结构1. 合并关联的 state2. 避免矛盾的 state3. 避免冗余的 state4. 避免重复的 state5避免深度嵌套的 State三、在组件间共享状态1. 什么是状态提升2. 实现状态提升的三个步骤3. 受控组件 vs. 非受控组件4. 单一数据源原则 (Single Source of Truth)5. 状态提升后的组件通信模型 总结语四、对 state 进行保留和重置1. 核心原则状态的“居住地”2. 状态何时会被【保留】3. 状态何时会被【重置】4. 如何【强制】重置状态5. 开发中的重要禁忌❌ 严禁嵌套定义组件总结摘要表五、迁移状态逻辑至 Reducer 中1. 什么是 Reducer迁移的三大步骤2. 核心语法对比useState 模式分散useReducer 模式解耦3. useState vs useReducer 深度对比4. 编写 Reducer 的两条“金律”核心保持 Reducer 的纯净 (Pure)逻辑Action 描述“交互”而非“数据更新”3. 快速决策指南六、使用 Context 深层传递参数1. 核心机制解决“Prop 逐级透传”2. 使用 Context 的三个标准步骤3. Context 的关键特性4. 决策指南何时使用优先考虑的替代方案最佳实践场景 深度思考与 CSS 属性继承的类比七、React 进阶Reducer Context 模式总结1. 核心价值为什么要结合使用2. 实现的三个关键步骤第一步创建 Context第二步提供 Context (Provider)第三步使用 Context (Consumer)3. 最佳实践模块化封装4. 代码模式对比一、使用 State 响应输入1. 命令式 vs 声明式命令式Imperative像给司机下达具体指令“过两个路口左转看到红房子停车”。你必须手动操作每一个 DOM 元素显示、隐藏、禁用。声明式Declarative像告诉出租车司机“去机场”。你只需要描述组件在不同状态下看起来是什么样React 会负责更新 DOM。2. 声明式地设计 UI 的 5 个步骤当你准备开发一个交互组件时请遵循以下流程第一步定位组件中不同的视图状态像设计师一样列出用户可能看到的所有视觉状态Empty: 初始状态按钮禁用。Typing: 正在输入按钮启用。Submitting: 提交中表单禁用显示加载动画。Success: 提交成功显示反馈信息。Error: 提交失败显示错误提示允许重试。第二步确定触发状态改变的因素人为输入点击按钮、切换输入框、点击链接。计算机输入网络请求成功/失败、定时器结束。第三步用 useState 表示内存中的 State先写下所有可能需要的变量即使它们看起来有重复const[answer,setAnswer]useState();const[error,setError]useState(null);const[isSubmitting,setIsSubmitting]useState(false);// ... 等等第四步删除不必要的 State精简逻辑审查你的 State问自己三个问题是否矛盾例如 isTyping 和 isSubmitting 同时为 true 是不可能的应合并为 status 变量。是否重复例如 isEmpty 可以通过 answer.length 0 计算得出。是否可以推导例如 isError 可以通过 error ! null 得到。精简后的结果示例const[answer,setAnswer]useState();const[error,setError]useState(null);const[status,setStatus]useState(typing);// typing, submitting, success第五步连接事件处理函数最后通过事件如 onChange, onSubmit调用 set 函数来切换状态。 核心心法总结UI 是状态的反映U I f ( s t a t e ) UI f(state)UIf(state)。状态机思维把组件想象成一个状态机明确每个状态下的视觉表现以及状态间的迁移路径。减少“不可能的状态”通过合并相关的变量如使用字符串状态机而非多个布尔值避免 UI 出现既在加载又在报错的尴尬情况。二、选择状态结构构建良好的 State 结构就像是为建筑搭建稳固的骨架。设计得当的 State 能显著减少 Bug并使组件更易于修改和调试。优化目标解决方案核心益处同步更新合并关联变量为对象防止更新遗漏消除冲突使用状态枚举 (Status)杜绝“既在加载又在成功”的矛盾保持简洁删除冗余/可推导的变量减少内存占用逻辑更清晰数据同步存储 ID 而非完整对象确保关联数据永远是最新的易于维护扁平化嵌套结构更新逻辑从 O(N) 降为 O(1)1. 合并关联的 state如果某两个 state 变量总是一起变化则将它们统一成一个 state 变量可能更好。❌ 差const [x, setX] useState(0); const [y, setY] useState(0);✅ 好const [position, setPosition] useState({ x: 0, y: 0 });提示更新对象 State 时必须使用setPosition({ ...position, x: 100 })展开运算符来显式复制其他属性。2. 避免矛盾的 state维度❌ 多个布尔值 (容易出错)✅ 状态枚举 (推荐)State 声明const [isSending, setIsSending] useState(false);const [isSent, setIsSent] useState(false);const [status, setStatus] useState(typing);逻辑维护手动同步开发者必须确保在一个变为true时另一个手动变为false。自动互斥只需要切换一个字符串值天然避免了状态重叠。可读性代码中充斥着复杂的布尔组合判断难以直观一眼看出当前阶段。通过语义化的状态名如sending直观判断当前 UI 阶段。健壮性可能出现isSending和isSent同时为true的逻辑 Bug。状态机模型确保在任何时刻只能处于一种确定的状态。3. 避免冗余的 state准则能算出来的就别存。坏处如果你把 fullName 存入 state你必须在 setFirstName 和 setLastName 的地方手动去更新它一旦漏掉一个数据就不同步了。const[firstName,setFirstName]useState();const[lastName,setLastName]useState();const[fullName,setFullName]useState();// ❌ 冗余正解 在渲染期间实时计算 const fullName firstName ’ ’ lastName;。不要在 state 中镜像 propsfunctionMessage({colorProp}){const[color,setColor]useState(colorProp);// 危险}原因如果父组件的 colorProp 变了子组件的 color State 不会随之更新因为它只在挂载时初始化一次。例外仅当你明确希望忽略后续更新时使用并建议改名为 initialColor。4. 避免重复的 state情景从一个列表中选择一项。错误把选中的整个对象存入 selectedItem。正确只存选中项的 id。const[selectedId,setSelectedId]useState(0);constselectedItemitems.find(itemitem.idselectedId);5避免深度嵌套的 State问题更新一个深层嵌套的地点如地球 - 亚洲 - 中国 - 北京需要复制每一层父级代码极其复杂。优化扁平化归一化。像数据库一样用 ID 索引来存储数据。三、在组件间共享状态1. 什么是状态提升状态提升是指将原本在子组件中各自维护的state移动到它们最近的公共父组件上。目的让多个子组件能够共享同一个数据源实现状态的同步更改。实现方式父组件保存state并通过props将数据和修改数据的函数回调函数传递给子组件。2. 实现状态提升的三个步骤当你发现两个组件需要同步变化时可以按以下节奏重构从子组件中移除 state删除子组件内部的useState改为从props接收数据。从父组件传递硬编码数据先在父组件中通过props给子组件传一个固定的值确保子组件能根据props正确渲染。在父组件添加真正的 state在父组件定义useState。将state变量作为 props 传给子组件。将改变 state 的函数如setActiveIndex封装成事件处理程序如onShow传给子组件。3. 受控组件 vs. 非受控组件这是一个重要的架构概念类型驱动源特点灵活性非受控组件内部state组件自己管理状态父组件难以干预。较低难以与其他组件同步受控组件外部props组件的行为完全由父组件驱动。较高容易组合和协调提示在实际开发中组件往往是混合的。有些 UI 交互如 CSS 悬停效果适合非受控而核心业务数据如表单值、当前激活项通常推荐受控。4. 单一数据源原则 (Single Source of Truth)定义对于应用中的每一个状态都应该由唯一一个组件来负责“掌控”它。原则不要在多个组件中复制相同的状态而是通过props向下流动或者通过状态提升向上移动。动态性状态的位置不是固定的。随着功能增加状态可能会在组件树中上下移动这是 React 开发中非常自然的重构过程。5. 状态提升后的组件通信模型向下通信数据驱动父组件通过props把state传给子组件子组件被动渲染。向上通信行为触发父组件通过props传给子组件一个回调函数。当子组件发生交互如点击按钮时调用该函数通知父组件修改state。 总结语状态提升是解决 React 组件间“步调不一致”的万金油。当你犹豫状态该放哪时问自己一句“谁需要知道这个状态”如果有两个兄弟组件都需要那就把它提到它们的公共父级那里去。四、对 state 进行保留和重置这份教程深入浅出地解释了 React 中 State 的生命周期与 UI 树位置之间的核心关系。1. 核心原则状态的“居住地”状态不在 JSX 里虽然你在组件内部编写useState但 State 实际上是由 React 内部保存的。关联依据位置React 根据组件在UI 渲染树中的位置将保存的状态与对应的组件关联起来。位置即“地址”你可以把 UI 树中的层级结构看作组件的“地址”。只要地址没变即使父组件更新了状态通常也会保留。2. 状态何时会被【保留】只要满足以下“双同”条件状态就会被保留相同的位置在父组件的子节点顺序/结构中处于同一层级。相同的组件类型标签名如Counter /没有发生改变。⚠️ 陷阱提醒React 关心的是渲染树中的最终结果而不是你代码里的if/else逻辑。即使你在不同的return分支里写了两个Counter /只要它们最终渲染在树的同一位置React 就会认为它们是同一个实例。3. 状态何时会被【重置】当以下任一情况发生时React 会销毁旧组件及其所有子树的状态组件被移除组件从渲染树中消失例如条件渲染为false。位置发生了不同类型的组件切换比如同一位置从Counter /变成了p。父级结构改变即使组件本身没变但它的父标签变了例如从被div包裹变成被section包裹。4. 如何【强制】重置状态有时即使组件位置没变我们也希望清除状态例如切换聊天对象时清空输入框。方法一改变组件位置不推荐通过逻辑让两个组件渲染在不同的层级位置但这会增加 JSX 的复杂度。方法二使用key推荐身份标识key不仅仅用于列表。给组件一个唯一的key如key{userId}可以告诉 React“即使我在同一个位置但我是一个全新的组件”。效果当key改变时React 会销毁旧的组件实例及其状态并创建一个全新的组件。5. 开发中的重要禁忌❌ 严禁嵌套定义组件错误做法不要在函数组件 A 的内部定义函数组件 B。后果每次 A 重新渲染时都会创建一个全新的 B 函数类型引用变了。现象这会导致 B 在每次父组件更新时都会彻底重置状态引发输入框失去焦点、性能下降等严重 Bug。总结摘要表场景React 的行为结果位置相同 类型相同保留状态状态延续位置不同销毁旧状态初始化新状态状态重置位置相同 类型改变销毁旧状态初始化新状态状态重置位置相同 类型相同 Key 改变视为不同组件状态重置五、迁移状态逻辑至 Reducer 中当组件的状态更新逻辑变得复杂且分散时使用useReducer可以将逻辑整合到组件外部的一个统一函数中即Reducer从而提高代码的可读性和可维护性。1. 什么是 ReducerReducer是处理状态的一种新方式。它将组件中“发生了什么”Action与“状态如何更新”State Logic分离开来。迁移的三大步骤Dispatch Action在事件处理程序中不再直接设置状态而是派发dispatch一个描述用户操作的 action 对象。编写 Reducer 函数编写一个外部函数根据不同的 Action 类型返回新的状态。在组件中使用使用useReducerHook 替换useState。2. 核心语法对比useState 模式分散functionhandleAddTask(text){setTasks([...tasks,{id:nextId,text,done:false}]);}functionhandleChangeTask(task){setTasks(tasks.map((t){if(t.idtask.id){returntask;}else{returnt;}}));}functionhandleDeleteTask(taskId){setTasks(tasks.filter((t)t.id!taskId));}useReducer 模式解耦第一步派发 ActionfunctionhandleAddTask(text){dispatch({type:added,id:nextId,text:text,});}functionhandleChangeTask(task){dispatch({type:changed,task:task,});}functionhandleDeleteTask(taskId){dispatch({type:deleted,id:taskId,});}第二步编写 Reducer (通常放在组件外部)functiontasksReducer(tasks,action){switch(action.type){caseadded:{return[...tasks,{id:action.id,text:action.text,done:false}];}casechanged:{returntasks.map(t(t.idaction.task.id?action.task:t));}casedeleted:{returntasks.filter(tt.id!action.id);}default:{throwError(未知 action: action.type);}}}第三步在组件中使用import{useReducer}fromreact;const[tasks,dispatch]useReducer(tasksReducer,initialTasks);3. useState vs useReducer 深度对比维度useStateuseReducer代码体积初始代码少。非常适合管理简单的布尔值、数字或字符串。初始代码较多。需要提前定义 Reducer 函数和各种 Action 类型。可读性当更新逻辑变得复杂时处理程序会变得臃肿可读性下降。将逻辑从组件中抽离实现关注点分离逻辑层级更清晰。调试体验出现问题时很难追踪是哪个函数在何时修改了状态。极其强大。通过打印Action 日志可以清晰看到每一个用户交互序列。测试便利性逻辑耦合在组件内通常需要配合组件挂载进行集成测试。极易测试。Reducer 是纯函数不依赖 React 环境可直接进行单元测试。4. 编写 Reducer 的两条“金律”为了确保应用的可预测性和易维护性在编写 Reducer 时必须遵循以下原则核心保持 Reducer 的纯净 (Pure)无副作用不要在 Reducer 内部发送 API 请求、执行定时器或进行任何影响外部的操作。不可变更新永远不要直接修改传入的state。必须通过展开运算符...或数组方法返回一个全新的对象/数组副本。确定性同样的输入State Action必须永远得到同样的输出。逻辑Action 描述“交互”而非“数据更新”单一交互原则一个 Action 应该对应一个具体的用户行为。示例如果用户点击“重置表单”应该派发一个type: reset_form。❌错误做法连续派发 5 个set_field的 Action。✅正确做法派发 1 个reset_formAction由 Reducer 一次性将 5 个字段全部重置。好处调试日志会非常清晰一眼就能看出用户点击了重置按钮。3. 快速决策指南使用useState状态是独立的如isOpen、逻辑简单、只有 1-2 个地方会修改它。使用useReducer状态逻辑复杂多个状态相互关联、多个事件处理程序以相似方式修改状态、或者需要维护大型对象/数组。六、使用 Context 深层传递参数Context 提供了一种在组件树中传递数据的方法无需在每个层级手动传递 props。1. 核心机制解决“Prop 逐级透传”在 React 中数据流通常是自上而下的单向数据流。痛点Prop Drilling当深层子组件需要祖先组件的数据时必须通过中间组件一层层手动传递 Props导致代码冗长且难以维护。对策ContextContext 建立了一个“广播系统”允许数据绕过中间组件直接从祖先组件“直达”任何深层的后代组件。2. 使用 Context 的三个标准步骤实现 Context 就像配置一个无线广播电台步骤动作关键语法Step 1: 建立电台创建并导出 Context 对象export const MyContext createContext(defaultValue);Step 2: 接收信号在子组件内通过 Hook读取const value useContext(MyContext);Step 3: 发射信号在父组件中提供数据MyContext value{yourValue} {children} /MyContext 版本注意在 React 最新版本中可以直接使用MyContext value{...}旧版本则需要使用MyContext.Provider value{...}。3. Context 的关键特性就近原则子组件总是从 UI 树中离它最近的那个 Provider 获取值。动态响应Context 的值通常与state绑定。当 Provider 的value改变时所有使用了useContext的子组件都会自动重新渲染。强穿透力Context 可以穿过任何中间组件即使中间组件是静态的或使用了React.memo不会被中断。高度独立不同的 Context 互不干扰。一个组件可以轻松消费多个不同的 Context如同时读取“主题”和“用户信息”。4. 决策指南何时使用Context 虽然强大但会使组件产生耦合降低复用性应谨慎使用。优先考虑的替代方案继续传递 Props如果层级较浅Props 是最清晰、最显式的数据流方式。组件组合 (Children)通过children属性将子组件传入利用“内容分发”减少中间传递层级。最佳实践场景全局偏好如深色/浅色模式Theme、语言国际化i18n。登录状态当前登录的用户信息、权限验证。路由/状态库许多库如 React Router, Redux底层都基于 Context 实现。复杂组件通信如折叠面板Accordion或选项卡Tabs父容器与深层子项共享交互状态。 深度思考与 CSS 属性继承的类比Context 的工作方式非常类似于 CSS 的属性继承如color或font-family你在根节点设置了color: blue所有子元素默认都变蓝。如果你在中间某个容器设置了color: green该容器内部的所有子元素都会“覆盖”掉蓝色变为绿色。这正是 Context 的精髓让组件能够“适应周围环境”并根据所处的 Context 渲染出不同的形态。七、React 进阶Reducer Context 模式总结将useReducer的状态管理逻辑与Context API的跨层级传递能力相结合是 React 中管理中大型应用状态的黄金搭档。1. 核心价值为什么要结合使用解决 Props 钻取Props Drilling避免将state和dispatch像接力棒一样穿过数十个中间组件只为了传给最深层的子组件。清晰的职责分离Reducer定义“如何更新状态”逻辑中心。Context定义“数据传给谁”广播中心。简化组件维护中间组件不再需要关心不属于它们的数据代码更整洁。2. 实现的三个关键步骤第一步创建 Context通常建议创建两个独立的 Context以优化性能TasksContext传递当前的状态数据State。TasksDispatchContext传递更新状态的函数Dispatch。注意分开存放可以确保那些只发送 Action 而不读取数据的组件在数据变化时不会被强制重新渲染。第二步提供 Context (Provider)在顶层组件中调用useReducer获取tasks和dispatch。使用嵌套的Provider将这两个值注入到组件树中。第三步使用 Context (Consumer)深层组件通过 Hook 获取所需资源useContext(TasksContext)获取数据。useContext(TasksDispatchContext)获取派发函数。3. 最佳实践模块化封装为了提高可重用性和安全性通常将逻辑封装在独立文件中如TasksContext.js封装 Provider 组件创建一个TasksProvider内部管理useReducer外部包裹{children}。自定义 Hook导出useTasks()和useTasksDispatch()。优势组件代码更简洁且可以在 Hook 中加入错误处理例如检查 Hook 是否在 Provider 外部被非法调用。4. 代码模式对比特性仅使用 ReducerReducer Context 模式状态存储存在于顶层组件存在于独立的 Provider 内部数据传递依靠Props逐层传递通过Context跨层级“瞬移”组件耦合子组件强依赖父组件传入的 Props子组件直接消费全局/局部 Context适用场景简单、层级浅的组件结构复杂、层级深、状态共享频繁的应用[!TIP]设计思想这种模式是Redux等主流状态管理库的核心灵魂。掌握了它你就理解了单向数据流和集中式状态管理的精髓未来迁移到 Redux 或 Zustand 将会非常轻松。