引子
对面下面代码中的组件现在有这样一个需求,点击按钮时候,<span>的数字加1。
functionMyContent(){return(<div><span>0</span><button>+</button></div>);}OK,根据咱们前面react事件处理中学到的,我们知道要在<button>上新增一个点击事件处理函数。这个函数触发时,改变一个变量的值,然后将这个变量放到<span>内。
使用变量更改数据的示例:
functionMyContent(){letindex=0;return(<div><span>{index}</span><button onClick={function(){index+=1;console.log(index);}}>+</button></div>);}上述代码显示在浏览器中时,咱们点击按钮,发现<span>中的数字并没有变化,反而在命令行中,有从1开始的数字被打印,说明咱们的思路是没问题的,就是代码执行过程中出了什么问题。
组件的状态
仔细想想上面的代码为什么没有达到预期的效果,其实就是在咱们申请的局部变量index改变之后,页面显示没有变,也就是之前咱们说起到的页面没有重新渲染。当然这里咱们需要重新渲染的仅仅是那个<span>内部的样式,而不是使用createRoot和root.render重新渲染整个的App组件,根组件重新渲染又会重置咱们组件中局部变量的值,所以根本不会显示变化。
除了使用代码重新渲染整个组件,React还会在组件的状态发生改变时重新渲染。而且这次渲染仅仅是状态变化后影响的函数组件的渲染。
这个过程是递归的:如果更新后的组件会返回某个另外的组件,那么React接下来就会渲染那个组件,而如果那个组件又返回了某个组件,那么React接下来就会渲染那个组件,以此类推。这个过程会持续下去,直到没有更多的嵌套组件并且React确切知道哪些东西应该显示到屏幕上为止。
讲到这里咱们似乎可以理解为什么在官方手册中建议我们将组件写成输入相同,输出相同的“纯函数”了。因为只有这样在React重新渲染的时候才能确定哪些组件发生了变化,需要重新渲染。
在React中,组件的状态(state)指的是组件内部持有的数据。它代表了可以在组件内部使用并随着时间更新的可变值。状态是React提供的一个特性,它使组件能够管理和更新自己的数据。它允许组件状态变化时重新渲染,确保用户界面反映了最新的数据。
要在React组件中定义状态,应该在组件内部使用useState钩子(Hook),然后可以在组件的方法或者JSX代码中访问和修改状态。当状态更新时,React将自动重新渲染组件及其子组件以反映这些变化。
React Hook是React 16.8版本引入的一个特性,它允许在函数组件中使用状态和其他React特性。Hook提供了一种在函数组件中实现类似类组件中管理组件状态和生命周期的方法,使函数组件更强大,更易于编写和理解。
在组件中使用useState定义状态的代码如下:
// 1. 从文件顶部的`React`中导入 useStateimport{useState}from'react';functionMyContent(){// let index = 0;const[index,setIndex]=useState(0);return(<div><span>{index}</span><button onClick={function(){setIndex(index+1);}}>+</button></div>);}Hooks ——以
use开头的函数——只能在组件或自定义Hook的最顶层调用。 你不能在条件语句、循环语句或其他嵌套函数内调用Hook。Hook 是函数,但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 “use” React 特性,类似于在文件顶部“导入”模块。
当调用useState时,是在告诉React想让这个组件记住一些东西:
const[index,setIndex]=useState(0);useState的唯一参数是state变量的初始值。在这个例子中,index的初始值被useState(0)设置为0。
返回值有两个,一个是state变量(index),它会保存上次渲染的值;另一个是state setter函数(setIndex)可以更新state变量并触发React重新渲染组件。
你可以在一个组件中拥有任意多种类型的state变量。state的类型可以是数字、字符串、布尔值、对象或数组等。
State是屏幕上组件实例内部的状态。换句话说,如果你渲染同一个组件两次,每个副本都会有完全隔离的state!改变其中一个不会影响另一个。同时,与props不同,state完全私有于声明它的组件。父组件无法更改它。这使你可以向任何组件添加或删除state,而不会影响其他组件。
state的特性
使用state setter函数设置state,相当于触发了请求组件的重新渲染。这意味着要让咱们的UI界面对外界的操作做出反应,就得相应的设置其state。
当React重新渲染一个组件的过程:
React会再次调用你的组件函数- 组件函数会返回新的JSX快照
React会更新界面以匹配返回的快照
作为一个组件的记忆,state并不像一个函数内部的局部变量,更像是在组件内存申请的单独存在于内存中的(堆区)变量,不会在咱们的组件函数返回之后消失。当React调用组件时,它会为特定的那一次渲染提供一张state快照。对应的组件会在其JSX中返回一张包含一整套新的props和事件处理函数的UI快照 ,其中所有的值都是 根据那一次渲染中state的值被计算出来的!
而且,使用setter设置state并不会立即直接改变state的值,React会等到事件处理函数中的所有代码都运行完毕再处理你的state更新。
比如下面的这个例子:
import{useState}from'react';exportdefaultfunctionCounter(){const[number,setNumber]=useState(0);return(<><h1>{number}</h1><button onClick={()=>{setNumber(number+1);setNumber(number+1);setNumber(number+1);}}>+3</button></>)}表面上看点击“+3”按钮之后会调用三次setNumber(number+1)函数,所以渲染后显示的数字应该是3。但是,如果你运行了就会发现,渲染后显示的state是1。
之所以造成这样的结果就是因为,设置state只会为下一次渲染变更state的值。在第一次渲染期间,number为0。尽管咱们调用了三次setNumber(number + 1),但在这次渲染的事件处理函数中number会一直是0,所以是三次运行都是将state设置成1。
还可以通过在心里把state变量替换成它们在你代码中的值来想象这个过程。由于 这次渲染 中的state变量number是0,其事件处理函数看起来会像这样:
<button onClick={()=>{setNumber(0+1);setNumber(0+1);setNumber(0+1);}}>+3</button>一个state变量的值永远不会在一次渲染的内部发生变化,即使其事件处理函数的代码是异步的。在那次渲染的onClick内部,number的值即使在调用setNumber(number + 1)之后也还是0。它的值在React通过调用你的组件“获取UI的快照”时就被“固定”了。
React会使state的值始终“固定”在一次渲染的各个事件处理函数内部。这种安排的优点是:咱们无需担心代码运行时state是否发生了变化。但是万一咱们想在重新渲染之前读取最新的state呢?
state的批处理机制
React之所以要让处理函数中的所有代码都彻底运行完毕之后再处理state值的更新,就是为了让浏览器可以更新多个state变量,甚至来自多个组件的state变量,而不会触发太多的重新渲染,这种设计称为“批处理”,批处理提高了React代码运行效率。但这也意味着只有在事件处理函数及其中任何代码执行完成之后,state及对应的UI才会更新。
如果咱们想在下次渲染之前多次更新同一个state,则可以像setNumber(n => n + 1)这样传入一个根据队列中的前一个state计算下一个state的函数,而不是像setNumber(number + 1)这样传入 下一个state值。这是一种告诉React“用state值做某事”而不是仅仅替换它的方法。
事件处理函数执行完成后,React将触发重新渲染。在重新渲染期间,React将处理队列。更新函数会在渲染期间执行,因此更新函数必须是纯函数并且只返回结果。千万不要尝试从它们内部设置state或者执行其他额外的功能。
React不会跨多个需要刻意触发的事件(如点击)进行批处理——每次点击都是单独处理的。React只会在一般来说安全的情况下才进行批处理。这可以确保,例如,如果第一次点击按钮会禁用表单,那么第二次点击就不会再次提交它。
import{useState}from'react';exportdefaultfunctionCounter(){const[number,setNumber]=useState(0);return(<><h1>{number}</h1><button onClick={()=>{setNumber(n=>n+1);setNumber(n=>n+1);setNumber(n=>n+1);}}>+3</button></>)}将代码改变之后,再次运行,点击按钮得到的值就是3了。
更新state中的对象和数组
前面的内容中提到了,state不仅仅存储数字还还可以存储其他类型的值。对于数字、字符串、布尔值这些JS的基本类型它们都是只读的,咱们不能直接去修改,而是要通过使用useState返回的设置函数去设置其值。
对于对象和数组来说一样的可以存储在组件的state中,也可以通过state setter去设置新的值,同样也得遵守返回的state不能直接修改的原则。当咱们想要更新一个对象或数组时,需要重新创建一个新的对象或数组,然后将其更新为这个新对象或数组。
当我们需要更新对象或者数组中的个别值时,需要将整个对象复制一份然后进行修改再进行设置state操作。
对于复杂的对象可以使用...对象展开语法来复制其他值,然后将修改的值放到最后进行覆盖:
setPerson({...person,// 复制上一个 person 中的所有字段firstName:e.target.value// 但是覆盖 firstName 字段});对于数组,如果咱们要更新数组中的某一个值,则更复杂一些。
对于新增可以使用这样的写法
setArtists(// 替换 state[// 是通过传入一个新数组实现的...artists,// 新数组包含原数组的所有元素{id:nextId++,name:name}// 并在末尾添加了一个新的元素]);这样就可以在数组最后新增一项,当然也可以在数组的开头新增一项:
setArtists(// 替换 state[// 是通过传入一个新数组实现的{id:nextId++,name:name},// 并在开头添加了一个新的元素...artists// 新数组包含原数组的所有元素]);如果需要向数组中的指定位置插入一个元素,可以借助slice()方法将数组切片后组合:
constinsertAt=1;// 可能是任何索引setArtists(// 替换 state[// 是通过传入一个新数组实现的...artists.slice(0,insertAt),// 新数组包含原数组insertAt之前的所有元素{id:nextId++,name:name},// 并在indexAt位置添加了一个新的元素...artists.slice(insertAt)// 新数组包含原数组insertAt及之后的所有元素]);删除数组中的某一项,可以使用filter方法生成一个新数组:
constindex=1;// 可能是任何索引setArtists(artists.filter(a=>a.id!==index));替换数组中的元素可以使用map进行重新生成,这里就不演示了。
对于数组和对象的设置需要说明的是,...展开语法本质是是“浅拷贝”——它只会复制一层,内部更深层次的依然是原本的state对象的一部分还是不能修改。这使得虽然它的执行速度很快,但是也意味着当你想要更新一个嵌套属性时,你必须得多次使用展开语法。
state使用技巧
在React中,不必直接去操作UI—— 不必直接启用、关闭、显示或隐藏组件。相反,咱们只需要声明想要显示的内容,React就会通过计算得出该如何去更新UI。
官方手册上提出的React实现UI的过程:
- 定位组件中不同的视图状态
- 确定是什么触发了这些
state的改变 - 表示内存中的
state(需要使用useState) - 删除任何不必要的
state变量 - 连接事件处理函数去设置
state
官方手册上提出的构建State的建议:
- 合并关联的
state。如果你总是同时更新两个或更多的state变量,请考虑将它们合并为一个单独的state变量。 - 避免互相矛盾的
state。当state结构中存在多个相互矛盾或“不一致”的state时,你就可能为此会留下隐患。应尽量避免这种情况。 - 避免冗余的
state。如果你能在渲染期间从组件的props或其现有的state变量中计算出一些信息,则不应将这些信息放入该组件的state中。 - 避免重复的
state。当同一数据在多个state变量之间或在多个嵌套对象中重复时,这会很难保持它们同步。应尽可能减少重复。 - 避免深度嵌套的
state。深度分层的state更新起来不是很方便。如果可能的话,最好以扁平化方式构建state。
在组件间共享状态
有时候,咱们希望两个组件的状态始终同步更改。要实现这一点,可以将相关state从这两个组件上移除,并把state放到它们的公共父级,再通过props将state传递给这两个组件。这被称为“状态提升”,这是编写React代码时常做的事。
在React应用中,很多组件都有自己的状态。一些状态可能“活跃”在叶子组件(树形结构最底层的组件)附近,例如输入框。另一些状态可能在应用程序顶部“活动”。例如,客户端路由库也是通过将当前路由存储在React状态中,利用props将状态层层传递下去来实现的!
对于每个独特的状态,都应该存在且只存在于一个指定的组件中作为state。这一原则也被称为拥有 “可信单一数据源”。它并不意味着所有状态都存在一个地方——对每个状态来说,都需要一个特定的组件来保存这些状态信息。你应该将状态提升到公共父级,或将状态传递到需要它的子级中,而不是在组件之间复制共享的状态。
你的应用会随着你的操作而变化。当你将State上下移动时,你依然会想要确定每个状态在哪里“活跃”。这都是开发过程的一部分!
对State进行保留和重置
当向一个组件添加状态时,那么可能会认为状态“存在”在组件内。但实际上,状态是由React保存的。React通过组件在渲染树中的位置将它保存的每个状态与正确的组件关联起来。
下面的例子中只有一个<Counter />JSX 标签,但它会在两个不同的位置渲染:
import{useState}from'react';exportdefaultfunctionApp(){constcounter=<Counter/>;return(<div>{counter}{counter}</div>);}functionCounter(){const[score,setScore]=useState(0);const[hover,setHover]=useState(false);letclassName='counter';if(hover){className+=' hover';}return(<div className={className}onPointerEnter={()=>setHover(true)}onPointerLeave={()=>setHover(false)}><h1>{score}</h1><button onClick={()=>setScore(score+1)}>加一</button></div>);}尽管看上去这是一个组件,但是这是两个独立的counter,因为它们在树中被渲染在了各自的位置。一般情况下你不用去考虑这些位置来使用React,但知道它们是如何工作会很有用。
在React中,屏幕中的每个组件都有完全独立的state。运行上面的代码,并试着点击两个counter你会发现它们互不影响。
只要一个组件还被渲染在UI树的相同位置,React就会一直保留它的state。 如果它被移除,或者一个不同的组件被渲染在相同的位置,那么React就会丢掉它的state。
一般来说,如果你想在重新渲染时保留state,几次渲染中的树形结构就应该相互“匹配”。结构不同就会导致state的销毁,因为React会在将一个组件从树中移除时销毁它的state。
默认情况下,React会在一个组件保持在同一位置时保留它的state。通常这就是咱们想要的,所以把它作为默认特性很合理。但有时候,你可能想要重置一个组件的state。考虑一下这个应用,它可以让两个玩家在每个回合中记录他们的得分:
import{useState}from'react';exportdefaultfunctionScoreboard(){const[isPlayerA,setIsPlayerA]=useState(true);return(<div>{isPlayerA?(<Counter person="Taylor"/>):(<Counter person="Sarah"/>)}<button onClick={()=>{setIsPlayerA(!isPlayerA);}}>下一位玩家!</button></div>);}functionCounter({person}){const[score,setScore]=useState(0);const[hover,setHover]=useState(false);letclassName='counter';if(hover){className+=' hover';}return(<div className={className}onPointerEnter={()=>setHover(true)}onPointerLeave={()=>setHover(false)}><h1>{person}的分数:{score}</h1><button onClick={()=>setScore(score+1)}>加一</button></div>);}目前当你切换玩家时,分数会被保留下来。这两个Counter出现在相同的位置,所以React会认为它们是 同一个Counter,只是传了不同的person prop。
但是从概念上讲,这个应用中的两个计数器应该是各自独立的。虽然它们在UI中的位置相同,但是一个是Taylor的计数器,一个是Sarah的计数器。
有两个方法可以在它们相互切换时重置 state:
- 将组件渲染在不同的位置
- 使用
key赋予每个组件一个明确的身份
方法1:将组件渲染在不同的位置
如果想让两个Counter各自独立的话,可以将它们渲染在不同的位置:
import{useState}from'react';exportdefaultfunctionScoreboard(){const[isPlayerA,setIsPlayerA]=useState(true);return(<div>{isPlayerA&&<Counter person="Taylor"/>}{!isPlayerA&&<Counter person="Sarah"/>}<button onClick={()=>{setIsPlayerA(!isPlayerA);}}>下一位玩家!</button></div>);}functionCounter({person}){const[score,setScore]=useState(0);const[hover,setHover]=useState(false);letclassName='counter';if(hover){className+=' hover';}return(<div className={className}onPointerEnter={()=>setHover(true)}onPointerLeave={()=>setHover(false)}><h1>{person}的分数:{score}</h1><button onClick={()=>setScore(score+1)}>加一</button></div>);}起初,isPlayerA的值是true,所以第一个位置包含了Counter的state,而第二个位置是空的。当点击“下一位玩家”按钮时,第一个位置会被清空,而第二个位置现在包含了一个Counter。每当Counter组件从DOM中被移除时,它的state会被销毁。这就是每次点击按钮它们就会被重置的原因。
这个解决方案在你只有少数几个独立的组件渲染在相同的位置时会很方便。这个例子中只有2个组件,所以在JSX里将它们分开进行渲染并不麻烦。
方法2:使用 key 来重置 state
在前面React基础知识的最后部分,咱们遇到过一个Each child in a list should have a unique "key" prop的报错。不得已咱们给渲染列表的每一项加了一个独一无二的key。
其实这个key不只可以用于列表!也可以使用key来让React区分任何组件。默认情况下,React使用父组件内部的顺序(“第一个计数器”、“第二个计数器”)来区分组件。但是key可以让你告诉React这不仅仅是first或者second计数器,而且还是一个特定的计数器——例如,Taylor的计数器。这样无论它出现在树的任何位置,React都会知道它是Taylor的计数器!
import{useState}from'react';exportdefaultfunctionScoreboard(){const[isPlayerA,setIsPlayerA]=useState(true);return(<div>{isPlayerA?(<Counter key="Taylor"person="Taylor"/>):(<Counter key="Sarah"person="Sarah"/>)}<button onClick={()=>{setIsPlayerA(!isPlayerA);}}>下一位玩家!</button></div>);}functionCounter({person}){const[score,setScore]=useState(0);const[hover,setHover]=useState(false);letclassName='counter';if(hover){className+=' hover';}return(<div className={className}onPointerEnter={()=>setHover(true)}onPointerLeave={()=>setHover(false)}><h1>{person}的分数:{score}</h1><button onClick={()=>setScore(score+1)}>加一</button></div>);}在Taylor和Sarah之间切换不会使state被保留下来。因为 你给他们赋了不同的key:
{isPlayerA?(<Counter key="Taylor"person="Taylor"/>):(<Counter key="Sarah"person="Sarah"/>)}指定一个key能够让React将key本身而非它们在父组件中的顺序作为位置的一部分。