news 2026/2/9 10:06:30

5.React状态管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
5.React状态管理

多更新的组件状态

在组件状态中,我们了解到了React中组件的状态及其用法。组件状态的主要作用就是由状态设置触发组件的局部UI渲染,状态用法也很简单。

有时候有些组件对于状态的更新操作很多,这就让我们很难短时间理清组件更新逻辑。示例如下:获取初始数据后,用户可以对其进行添加、删除和修改操作。

import{useState}from'react';functionAddTask({onAddTask}){const[text,setText]=useState('');return(<><input placeholder="Add task"value={text}onChange={e=>setText(e.target.value)}/><button onClick={()=>{setText('');onAddTask(text);}}>Add</button></>);}functionTaskList({tasks,onChangeTask,onDeleteTask}){return(<ul>{tasks.map(task=>(<li key={task.id}><Task task={task}onChange={onChangeTask}onDelete={onDeleteTask}/></li>))}</ul>);}functionTask({task,onChange,onDelete}){const[isEditing,setIsEditing]=useState(false);lettaskContent;if(isEditing){taskContent=(<><input value={task.text}onChange={e=>{onChange({...task,text:e.target.value})}}/><button onClick={()=>setIsEditing(false)}>Save</button></>);}else{taskContent=(<>{task.text}<button onClick={()=>setIsEditing(true)}>Edit</button></>);}return(<label><input type="checkbox"checked={task.done}onChange={e=>{onChange({...task,done:e.target.checked});}}/>{taskContent}<button onClick={()=>{onDelete(task.id)}}>Delete</button></label>);}exportdefaultfunctionTaskApp(){const[tasks,setTasks]=useState(initialTasks);functionhandleAddTask(text){setTasks([...tasks,{id:nextId++,text:text,done:false,},]);}functionhandleChangeTask(task){setTasks(tasks.map((t)=>{if(t.id===task.id){returntask;}else{returnt;}}));}functionhandleDeleteTask(taskId){setTasks(tasks.filter((t)=>t.id!==taskId));}return(<><h1>布拉格的行程安排</h1><AddTask onAddTask={handleAddTask}/><TaskList tasks={tasks}onChangeTask={handleChangeTask}onDeleteTask={handleDeleteTask}/></>);}letnextId=3;constinitialTasks=[{id:0,text:'参观卡夫卡博物馆',done:true},{id:1,text:'看木偶戏',done:false},{id:2,text:'打卡列侬墙',done:false},];

上面的代码中有三个不同的事件处理程序来实现状态数据的添加、删除和修改。这个组件的每个事件处理程序都通过setTasks来更新状态。随着这个组件的不断迭代,其状态逻辑可能也会越来越多,随着代码也会越变越复杂。

为了降低这种复杂度,并让所有逻辑都可以存放在一个易于理解的地方,你可以将这些状态逻辑移到组件之外的一个称为reducer的函数中。

reducer状态管理的另一种方式

reducerReact中处理状态的另一种方式,使用一个useReducerHook来实现,主要用于管理复杂的状态变化逻辑。功能上与state类似,但是代码形式上有些不同。两种方式各有优劣,简单状态下使用useState管理组件状态即可,对于复杂状态下,可以尝试将useState转换为useReducer

至于State的优劣和取舍,在咱们进行一次useSateuseReducer的转换后就能总结出来。转换过程可以分为三步:

  1. 将设置状态的逻辑修改dispatch的一个action
  2. 编写一个reducer函数
  3. 在组件中使用reducer

Step1: 将设置状态的逻辑修改成dispatch的一个action

这一步,咱们先移除所有的状态设置逻辑,只留下三个事件处理函数:

  1. handleAddTask(text): 在用户点击“添加”时被调用。
  2. handleChangeTask(task): 在用户切换任务或点击“保存”时被调用。
  3. handleDeleteTask(taskId): 在用户点击“删除”时被调用

使用reducer管理状态与直接设置状态略有不同。它不是通过设置状态来告诉React“要做什么”,而是通过事件处理程序dispatch一个“action” 来指明 “用户刚刚做了什么”。(而状态更新逻辑则保存在其他地方!)因此,我们不再通过事件处理器直接 “设置task”,而是dispatch一个 “添加/修改/删除任务” 的action。这更加符合用户的思维。

functionhandleAddTask(text){dispatch({type:'added',id:nextId++,text:text,});}functionhandleChangeTask(task){dispatch({type:'changed',task:task,});}functionhandleDeleteTask(taskId){dispatch({type:'deleted',id:taskId,});}

传递给dispatch函数的就叫action,这是一个普通的JavaScript对象。它的结构是由咱们自主定义的,里面可以存储一些有用的参数,通常情况下,它应该至少包含可以表明“发生了什么事情”的信息(上述代码中的type字段),这个信息将指导dispatch的函数去处理状态变化。

Step2: 编写一个reducer函数

reducer函数就是你放置状态逻辑的地方。它接受两个参数,分别为当前stateaction对象,并且返回的是更新后的state

functiontasksReducer(tasks,action){switch(action.type){case'added':{return[...tasks,{id:action.id,text:action.text,done:false,},];}case'changed':{returntasks.map((t)=>{if(t.id===action.task.id){returnaction.task;}else{returnt;}});}case'deleted':{returntasks.filter((t)=>t.id!==action.id);}default:{throwError('未知 action: '+action.type);}}}

由于reducer函数接受state作为参数,因此你可以 在组件之外声明它。

Step3: 在组件中使用reducer

最后咱们需要将上一步实现的tasksReducer导入到组件中。

// 1. 首先从`React`中导入`useReducer`// import { useState } from 'react';import{useReducer}from'react';// 2. 组件中使用`useReducer`替换掉`useSate`// const [tasks, setTasks] = useState(initialTasks);const[tasks,dispatch]=useReducer(tasksReducer,initialTasks);

useState类似,咱们必须给useReducer传递一个初始状态,它会返回一个状态值(tasks)和一个设置状态的函数(dispatch)。但是useReducer还需要传入一个用于处理各种action的函数(tasksReducer)。

最后得到的代码是这样的:

import{useReducer}from'react';importAddTaskfrom'./AddTask.js';importTaskListfrom'./TaskList.js';exportdefaultfunctionTaskApp(){const[tasks,dispatch]=useReducer(tasksReducer,initialTasks);functionhandleAddTask(text){dispatch({type:'added',id:nextId++,text:text,});}functionhandleChangeTask(task){dispatch({type:'changed',task:task,});}functionhandleDeleteTask(taskId){dispatch({type:'deleted',id:taskId,});}return(<><h1>布拉格的行程安排</h1><AddTask onAddTask={handleAddTask}/><TaskList tasks={tasks}onChangeTask={handleChangeTask}onDeleteTask={handleDeleteTask}/></>);}functiontasksReducer(tasks,action){switch(action.type){case'added':{return[...tasks,{id:action.id,text:action.text,done:false,},];}case'changed':{returntasks.map((t)=>{if(t.id===action.task.id){returnaction.task;}else{returnt;}});}case'deleted':{returntasks.filter((t)=>t.id!==action.id);}default:{throwError('未知 action: '+action.type);}}}letnextId=3;constinitialTasks=[{id:0,text:'参观卡夫卡博物馆',done:true},{id:1,text:'看木偶戏',done:false},{id:2,text:'打卡列侬墙',done:false}];

现在,事件处理函数只通过派发action来指定发生了什么,而reducer函数通过响应actions来决定状态如何更新。这样一来,状态修改的全部内容就集中到了tasksReducer函数中了,我们也能一眼看出状态经历了哪些改变。

对比useStateuseReducer

Reducer并非没有缺点!以下是比较它们的几个方面:

  1. 代码体积: 通常,在使用useState时,一开始只需要编写少量代码。而useReducer必须提前编写reducer函数和需要调度的actioins。但是,当多个事件处理程序以相似的方式修改state时,useReducer可以减少代码量。
  2. 可读性: 当状态更新逻辑足够简单时,useState的可读性还行。但是,一旦逻辑变得复杂起来,它们会使组件变得臃肿且难以阅读。在这种情况下,useReducer允许你将状态更新逻辑与事件处理程序分离开来。
  3. 可调试性: 当使用useState出现问题时, 你很难发现具体原因。而使用useReducer时,你可以在reducer函数中通过打印日志的方式来观察每个状态的更新,以及为什么要更新(来自哪个action)。 如果所有action都没问题,你就知道问题出在了reducer本身的逻辑中。 然而,与使用useState相比,你必须单步执行更多的代码。
  4. 可测试性:reducer是一个不依赖于组件的纯函数。这就意味着你可以单独对它进行测试。一般来说,我们最好是在真实环境中测试组件,但对于复杂的状态更新逻辑,针对特定的初始状态和action,断言reducer返回的特定状态会很有帮助。

如果你在修改某些组件状态时经常出现问题或者想给组件添加更多逻辑时,建议你还是使用reducer。当然,也不必整个项目都用reducer,这是可以自由搭配的。

编写一个好的reducer

编写reducer时最好牢记以下两点:

  • reducer必须是纯粹的。 这一点和状态更新函数是相似的,reducer是在渲染时运行的!(actions会排队直到下一次渲染)。这就意味着reducer必须纯净,即当输入相同时,输出也是相同的。它们不应该包含异步请求、定时器或者任何副作用(对组件外部有影响的操作)。它们应该以不可变值的方式去更新对象数组
  • 每个action都描述了一个单一的用户交互,即使它会引发数据的多个变化。举个例子,如果用户在一个由reducer管理的表单(包含五个表单项)中点击了重置按钮,那么dispatch一个reset_formactiondispatch五个单独的set_fieldaction更加合理。如果你在一个reducer中打印了所有的action日志,那么这个日志应该是很清晰的,它能让你以某种步骤复现已发生的交互或响应。这对代码调试很有帮助!

使用Context深层传递参数

通常来说,咱们会通过props将信息从父组件传递到子组件。但是,如果必须通过许多中间组件向下传递props,或是在应用中的许多组件需要相同的信息,传递props会变得十分冗长和不便。Context允许父组件向其下层无论多深的任何组件提供信息,而无需通过props显式传递。Context翻译过来时上下文,它可以为父组件下面的整个组件树提供数据,咱们可以将其看成父组件下的一个全局变量。

这是一个来自官方手册的例子,要实现的是Heading组件接收一个level参数来决定它标题尺寸:

functionHeading({level,children}){switch(level){case1:return<h1>{children}</h1>;case2:return<h2>{children}</h2>;case3:return<h3>{children}</h3>;case4:return<h4>{children}</h4>;case5:return<h5>{children}</h5>;case6:return<h6>{children}</h6>;default:throwError('未知的 level:'+level);}}functionSection({children}){return(<section className="section">{children}</section>);}exportdefaultfunctionPage(){return(<Section><Heading level={1}>主标题</Heading><Heading level={2}>副标题</Heading><Heading level={3}>子标题</Heading><Heading level={4}>子子标题</Heading><Heading level={5}>子子子标题</Heading><Heading level={6}>子子子子标题</Heading></Section>);}

假设先要让相同Section中的多个Heading具有相同的尺寸,对于这样一个复杂的结构Page组件可能就会写成这样:

exportdefaultfunctionPage(){return(<Section><Heading level={1}>主标题</Heading><Section><Heading level={2}>副标题</Heading><Heading level={2}>副标题</Heading><Heading level={2}>副标题</Heading><Section><Heading level={3}>子标题</Heading><Heading level={3}>子标题</Heading><Heading level={3}>子标题</Heading><Section><Heading level={4}>子子标题</Heading><Heading level={4}>子子标题</Heading><Heading level={4}>子子标题</Heading></Section></Section></Section></Section>);}

目前,你将level参数分别传递给每个<Heading>,将level参数传递给<Section>组件而不是传给<Heading>组件看起来更好一些。这样的话你可以强制使同一个section中的所有标题都有相同的尺寸:

<Section level={3}><Heading>关于</Heading><Heading>照片</Heading><Heading>视频</Heading></Section>

但是<Heading>组件是如何知道离它最近的<Section>level的呢?这需要子组件可以通过某种方式“访问”到组件树中某处在其上层的数据

这个功能不能只通过props来实现它。这就是context大显身手的地方。你可以通过以下三个步骤来实现它:

  1. 创建一个context。(你可以将其命名为LevelContext, 因为它表示的是标题级别。)
  2. 在需要数据的组件内使用刚刚创建的context。(Heading将会使用LevelContext。)
  3. 在指定数据的组件中提供这个context。(Section将会提供LevelContext。)

Context可以让父节点,甚至是很远的父节点都可以为其内部的整个组件树提供数据。

Step 1: 创建context

Context在调用前需要在组件外创建,甚至可以在单独的文件中创建它,如果你需要在单独文件中创建并将其从一个文件中导出,可以采用如下的代码:

import{createContext}from'react';exportconstLevelContext=createContext(1);

createContext只需默认值这么一个参数。在这里,1表示最大的标题级别,但是也可以传递任何类型的值(甚至可以传入一个对象)。

Step 2: 使用Context

React中引入useContext Hook以及你刚刚创建的context:

import{useContext}from'react';import{LevelContext}from'./LevelContext.js';

删掉level参数并从你刚刚引入的LevelContext中读取值:

functionHeading({children}){constlevel=useContext(LevelContext);switch(level){case1:return<h1>{children}</h1>;case2:return<h2>{children}</h2>;case3:return<h3>{children}</h3>;case4:return<h4>{children}</h4>;case5:return<h5>{children}</h5>;case6:return<h6>{children}</h6>;default:throwError('未知的 level:'+level);}}

useContext是一个Hook。和useState以及useReducer一样,你只能在React组件中(不是循环或者条件里)立即调用HookuseContext告诉React Heading组件想要读取LevelContext

现在Heading组件没有level参数,你不需要再像这样在你的JSX中将level参数传递给Heading,修改一下JSX,让Section组件代替Heading组件接收level参数:

import{createContext}from'react';import{useContext}from'react';constLevelContext=createContext(1);functionHeading({children}){constlevel=useContext(LevelContext);switch(level){case1:return<h1>{children}</h1>;case2:return<h2>{children}</h2>;case3:return<h3>{children}</h3>;case4:return<h4>{children}</h4>;case5:return<h5>{children}</h5>;case6:return<h6>{children}</h6>;default:throwError('未知的 level:'+level);}}functionSection({children}){return(<section className="section">{children}</section>);}exportdefaultfunctionPage(){return(<Section level={1}><Heading>主标题</Heading><Section level={2}><Heading>副标题</Heading><Heading>副标题</Heading><Heading>副标题</Heading><Section level={3}><Heading>子标题</Heading><Heading>子标题</Heading><Heading>子标题</Heading><Section level={4}><Heading>子子标题</Heading><Heading>子子标题</Heading><Heading>子子标题</Heading></Section></Section></Section></Section>);}

注意!这个示例运行起来还达不到咱们的预期。所有Headings的尺寸都一样,因为 即使你正在使用context,但是你还没有提供它。如果不提供contextReact会使用在上一步指定的默认值。

在这个例子中,为createContext传入了1这个参数,所以useContext(LevelContext)会返回1,把所有的标题都设置为<h1>。我们通过让每个Section提供它自己的context来修复这个问题。

Step 3:提供 context

section组件目前渲染传入它的子组件,把它们用context provider包裹起来 以提供LevelContext给它们:

functionSection({level,children}){return(<section className="section"><LevelContext value={level}>{children}</LevelContext></section>);}

这告诉React:“如果在<Section>组件中的任何子组件请求LevelContext,给他们这个level。”组件会使用UI树中在它上层最近的那个<LevelContext>传递过来的值。

这与原始代码的运行结果相同,但是你不需要向每个Heading组件传递level参数了!取而代之的是,它通过访问上层最近的Section来“断定”它的标题级别:

  1. 你将一个level参数传递给<Section>
  2. Section把它的子元素包在<LevelContext value={level}>里面。
  3. Heading使用useContext(LevelContext)访问上层最近的LevelContext提供的值。

在相同的组件中使用并提供context:

目前,你仍需要手动指定每个sectionlevel。由于context让你可以从上层的组件读取信息,每个Section都会从上层的Section读取level,并自动向下层传递level + 1。你可以像下面这样做:

import{useContext}from'react';functionSection({children}){constlevel=useContext(LevelContext);return(<section className="section"><LevelContext value={level+1}>{children}</LevelContext></section>);}

这样修改之后,你不用将level参数传给<Section>或者是<Heading>了:

import{createContext}from'react';import{useContext}from'react';constLevelContext=createContext(1);functionHeading({children}){constlevel=useContext(LevelContext);switch(level){case1:return<h1>{children}</h1>;case2:return<h2>{children}</h2>;case3:return<h3>{children}</h3>;case4:return<h4>{children}</h4>;case5:return<h5>{children}</h5>;case6:return<h6>{children}</h6>;default:throwError('未知的 level:'+level);}}functionSection({children}){constlevel=useContext(LevelContext);return(<section className="section"><LevelContext value={level+1}>{children}</LevelContext></section>);}exportdefaultfunctionPage(){return(<Section><Heading>主标题</Heading><Section><Heading>副标题</Heading><Heading>副标题</Heading><Heading>副标题</Heading><Section><Heading>子标题</Heading><Heading>子标题</Heading><Heading>子标题</Heading><Section><Heading>子子标题</Heading><Heading>子子标题</Heading><Heading>子子标题</Heading></Section></Section></Section></Section>);}

现在,HeadingSection都通过读取LevelContext来判断它们的深度。而且Section把它的子组件都包在LevelContext中来指定其中的任何内容都处于一个“更深”的级别。

Context 会穿过中间层级的组件

可以在提供context的组件和使用它的组件之间的层级插入任意数量的组件。这包括像<div>这样的内置组件和自己创建的组件。

Context让你可以编写“适应周围环境”的组件,并且根据在哪(或者说在哪个context中)来渲染它们不同的样子。

Context的工作方式可能会让人想起CSS属性继承。在CSS中,你可以为一个<div>手动指定color: blue,并且其中的任何DOM节点,无论多深,都会继承那个颜色,除非中间的其他DOM节点用color: green来覆盖它。类似地,在React中,覆盖来自上层的某些context的唯一方法是将子组件包裹到一个提供不同值的context provider中。

CSS中,诸如colorbackground-color之类的不同属性不会覆盖彼此。你可以设置所有<div>color为红色,而不会影响background-color。类似地,不同的React context不会覆盖彼此。你通过createContext()创建的每个context都和其他context完全分离,只有使用和提供 那个特定的context的组件才会联系在一起。一个组件可以轻松地使用或者提供许多不同的context

但是context对于许多其他的场景也很有用。你可以用它来传递整个子树需要的任何信息:当前的颜色主题、当前登录的用户等。

使用Context之前

使用Context看起来非常方便诱人!这也意味着它也太容易被过度使用了。如果咱们只想把一些props传递到多个层级中,这并不意味着你需要把这些信息放到context里。

在使用context之前,可以考虑以下几种替代方案:

  1. 传递 props开始。 如果组件看起来不起眼,那么通过十几个组件向下传递一堆props并不罕见。这有点像是在埋头苦干,但是这样做可以让哪些组件用了哪些数据变得十分清晰!维护你代码的人会很高兴你用props让数据流变得更加清晰。
  2. 抽象组件并将 JSX 作为 children 传递给它们。 如果通过很多层不使用该数据的中间组件(并且只会向下传递)来传递数据,这通常意味着在此过程中忘记了抽象组件。举个例子,可能在想传递一些像posts的数据props到不会直接使用这个参数的组件,类似<Layout posts={posts} />。取而代之的是,让Layoutchildren当做一个参数,然后渲染<Layout><Posts posts={posts} /></Layout>。这样就减少了定义数据的组件和使用数据的组件之间的层级。

如果这两种方法都不适合你,再考虑使用context

Context的使用场景

  • 主题:如果你的应用允许用户更改其外观(例如暗夜模式),可以在应用顶层放一个context provider,并在需要调整其外观的组件中使用该context
  • 当前账户:许多组件可能需要知道当前登录的用户信息。将它放到context中可以方便地在树中的任何位置读取它。某些应用还允许你同时操作多个账户(例如,以不同用户的身份发表评论)。在这些情况下,将UI的一部分包裹到具有不同账户数据的provider中会很方便。
  • 路由:大多数路由解决方案在其内部使用context来保存当前路由。这就是每个链接“知道”它是否处于活动状态的方式。如果你创建自己的路由库,你可能也会这么做。
  • 状态管理: 随着你的应用的增长,最终在靠近应用顶部的位置可能会有很多state。许多遥远的下层组件可能想要修改它们。通常 将reducercontext搭配使用来管理复杂的状态并将其传递给深层的组件来避免过多的麻烦。

Context不局限于静态值。如果你在下一次渲染时传递不同的值,React将会更新读取它的所有下层组件!这就是context经常和state结合使用的原因。

使用ReducerContext扩展你的应用

Reducer可以整合组件的状态更新逻辑。Context可以将信息深入传递给其他组件。你可以组合使用它们来共同管理一个复杂页面的状态。

结合使用reducercontext

reducer介绍的例子里面,状态被reducer所管理。reducer函数包含了所有的状态更新逻辑并在此文件的底部声明:

Reducer有助于保持事件处理程序的简短明了。但随着应用规模越来越庞大,你就可能会遇到别的困难。目前,tasks状态和dispatch函数仅在顶级TaskApp组件中可用。要让其他组件读取任务列表或更改它,你必须显式传递当前状态和事件处理程序,将其作为props

例如,TaskApp将一系列task和事件处理程序传递给TaskList

<TaskList task={tasks}onChangeTask={handleChangeTask}onDeleteTask={handleDeleteTask}/>

TaskList将事件处理程序传递给Task

<Task task={task}onChange={onChangeTask}onDelete={onDeleteTask}/>

在像这样的小示例里这样做没什么问题,但是如果你有成百上千个中间组件,传递所有状态和函数可能会非常麻烦!

刚刚学过Context之后,咱们有了解决方案,可以直接把tasks状态和dispatch函数都 放入context。这样,所有的在TaskApp组件树之下的组件都不必一直往下传props而可以直接读取tasksdispatch函数。

下面将介绍如何结合使用reducercontext

  1. 创建context
  2. statedispatch放入context
  3. 在组件树的任何地方使用context

Step 1: 创建context

useReducer返回当前的tasksdispatch函数来让你更新它们:

const[tasks,dispatch]=useReducer(tasksReducer,initialTasks);

为了将它们从组件树往下传,你将创建两个不同的context:

  • TasksContext提供当前的tasks列表。
  • TasksDispatchContext提供了一个函数可以让组件分发动作。
import{createContext}from'react';constTasksContext=createContext(null);constTasksDispatchContext=createContext(null);

在这里,先把null作为默认值传递给两个context。实际值是由TaskApp组件提供的。

Step 2: 将statedispatch函数放入context

现在,你可以将所有的context导入TaskApp组件。获取useReducer()返回的tasksdispatch并将它们提供给整个组件树:

import{createContext}from'react';constTasksContext=createContext(null);constTasksDispatchContext=createContext(null);exportdefaultfunctionTaskApp(){const[tasks,dispatch]=useReducer(tasksReducer,initialTasks);// ...return(<TasksContext value={tasks}><TasksDispatchContext value={dispatch}>// ...</TasksDispatchContext></TasksContext>);}

现在,你可以同时通过propscontext传递信息:

Step 3: 在组件树中的任何地方使用context

现在你不需要将tasks和事件处理程序在组件树中传递,相反,任何需要tasks的组件都可以从TasksContext中读取它,任何组件都可以从context中读取dispatch函数并调用它,从而更新任务列表。TaskApp组件不会向下传递任何事件处理程序,TaskList也不会。每个组件都会读取它需要的context:

import{createContext}from'react';import{useState,useContext}from'react';import{useReducer}from'react';constTasksContext=createContext(null);constTasksDispatchContext=createContext(null);functionTaskList(){consttasks=useContext(TasksContext);return(<ul>{tasks.map(task=>(<li key={task.id}><Task task={task}/></li>))}</ul>);}functionTask({task}){const[isEditing,setIsEditing]=useState(false);constdispatch=useContext(TasksDispatchContext);lettaskContent;if(isEditing){taskContent=(<><input value={task.text}onChange={e=>{dispatch({type:'changed',task:{...task,text:e.target.value}});}}/><button onClick={()=>setIsEditing(false)}>Save</button></>);}else{taskContent=(<>{task.text}<button onClick={()=>setIsEditing(true)}>Edit</button></>);}return(<label><input type="checkbox"checked={task.done}onChange={e=>{dispatch({type:'changed',task:{...task,done:e.target.checked}});}}/>{taskContent}<button onClick={()=>{dispatch({type:'deleted',id:task.id});}}>Delete</button></label>);}functionAddTask(){const[text,setText]=useState('');constdispatch=useContext(TasksDispatchContext);return(<><input placeholder="Add task"value={text}onChange={e=>setText(e.target.value)}/><button onClick={()=>{setText('');dispatch({type:'added',id:nextId++,text:text,});}}>Add</button></>);}letnextId=3;exportdefaultfunctionTaskApp(){const[tasks,dispatch]=useReducer(tasksReducer,initialTasks);return(<TasksContext value={tasks}><TasksDispatchContext value={dispatch}><h1>Day offinKyoto</h1><AddTask/><TaskList/></TasksDispatchContext></TasksContext>);}functiontasksReducer(tasks,action){switch(action.type){case'added':{return[...tasks,{id:action.id,text:action.text,done:false}];}case'changed':{returntasks.map(t=>{if(t.id===action.task.id){returnaction.task;}else{returnt;}});}case'deleted':{returntasks.filter(t=>t.id!==action.id);}default:{throwError('Unknown action: '+action.type);}}}constinitialTasks=[{id:0,text:'Philosopher’s Path',done:true},{id:1,text:'Visit the temple',done:false},{id:2,text:'Drink matcha',done:false}];

state仍然 “存在于” 顶层Task组件中,由useReducer进行管理。不过,组件树里的组件只要导入这些context之后就可以获取tasksdispatch

将相关逻辑迁移到一个文档当中

这不是必须的,但你可以通过将reducercontext移动到单个文件中来进一步整理组件。新建一个“TasksContext.js” 文件包含两个context声明,来给这个文件添加更多代码,将reducer移动到此文件中,然后声明一个新的TaskProvider组件。此组件将所有部分连接在一起:

  1. 它将管理reducer的状态
  2. 它将提供现有的context给组件树
  3. 它将把children作为prop,所以你可以传递JSX。
import{createContext,useReducer}from'react';exportconstTasksContext=createContext(null);exportconstTasksDispatchContext=createContext(null);exportfunctionTasksProvider({children}){const[tasks,dispatch]=useReducer(tasksReducer,initialTasks);return(<TasksContext value={tasks}><TasksDispatchContext value={dispatch}>{children}</TasksDispatchContext></TasksContext>);}functiontasksReducer(tasks,action){switch(action.type){case'added':{return[...tasks,{id:action.id,text:action.text,done:false}];}case'changed':{returntasks.map(t=>{if(t.id===action.task.id){returnaction.task;}else{returnt;}});}case'deleted':{returntasks.filter(t=>t.id!==action.id);}default:{throwError('Unknown action: '+action.type);}}}constinitialTasks=[{id:0,text:'Philosopher’s Path',done:true},{id:1,text:'Visit the temple',done:false},{id:2,text:'Drink matcha',done:false}];

你也可以从TasksContext.js中导出使用context的函数:

exportfunctionuseTasks(){returnuseContext(TasksContext);}exportfunctionuseTasksDispatch(){returnuseContext(TasksDispatchContext);}

组件可以通过以下函数读取context

consttasks=useTasks();constdispatch=useTasksDispatch();

这不会改变任何行为,但它会允许你之后进一步分割这些context或向这些函数添加一些逻辑。现在所有的contextreducer连接部分都在TasksContext.js中。这保持了组件的干净和整洁,让我们专注于它们显示的内容,而不是它们从哪里获得数据:

你可以将TasksProvider视为页面的一部分,它知道如何处理tasksuseTasks用来读取它们,useTasksDispatch用来从组件树下的任何组件更新它们。

useTasksuseTasksDispatch这样的函数被称为自定义 Hook。 如果你的函数名以use开头,它就被认为是一个自定义Hook。这让你可以使用其他Hook,比如useContext

随着应用的增长,你可能会有许多这样的contextreducer的组合。这是一种强大的拓展应用并提升状态的方式,让你在组件树深处访问数据时无需进行太多工作。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/5 4:59:11

Wan2.2-T2V-A14B支持多种艺术风格迁移的实现方式

Wan2.2-T2V-A14B&#xff1a;如何实现多艺术风格视频生成 在短视频内容爆炸式增长的今天&#xff0c;品牌方、创作者和影视团队面临的最大挑战之一不再是“有没有创意”&#xff0c;而是“如何快速、低成本地将创意可视化”。传统视频制作流程动辄数周周期、高昂成本&#xff0…

作者头像 李华
网站建设 2026/1/30 20:02:07

哔哩下载姬实战手册:从零到精通的B站视频管理技巧

还记得那个让你抓狂的场景吗&#xff1f;收藏夹里心爱的视频突然下架&#xff0c;精心整理的UP主内容无法离线观看&#xff0c;或者急需某个视频素材却发现网络不稳定。这些痛点正是哔哩下载姬要帮你解决的现实问题。 【免费下载链接】downkyi 哔哩下载姬downkyi&#xff0c;哔…

作者头像 李华
网站建设 2026/2/5 20:01:40

不是吧,都2025年了你别说你还不会Spring MVC基本应用

1.1 经典三层结构 在JavaEE开发中&#xff0c;几乎全部都是基于B/S架构的开发。那么在B/S架构中&#xff0c;系统标准的三层架构包括&#xff1a;表现层、业务层、持久层。三层架构在我们的实际开发中使用得非常多&#xff0c;接下来我们详细了解下这三层架构。 表现层&#…

作者头像 李华
网站建设 2026/1/28 18:27:40

Wan2.2-T2V-A14B是否开放LoRA微调接口?社区开发者关注焦点

Wan2.2-T2V-A14B是否开放LoRA微调接口&#xff1f;社区开发者关注焦点 在AI生成内容&#xff08;AIGC&#xff09;浪潮席卷全球的今天&#xff0c;文本到视频&#xff08;Text-to-Video, T2V&#xff09;技术正从实验室走向实际生产环境。相比图像生成&#xff0c;视频生成不仅…

作者头像 李华