当我们使用react时,我们经常需要在组件的输入道具更新时重新渲染它。常见的方法是在componentWillReceiveProps中将新的道具更新到组件的状态(这种状态称为派生状态),从而实现重渲染。在React 16.3中,引入了一个新的钩子函数getDerivedStateFromProps来满足这个需求。然而,无论是使用componentWillReceiveProps还是getDerivedStateFromProps,都没有那么优雅,容易出错。所以今天我们来讨论一下这种实现会产生的问题以及更好的实现方案。
何时使用派生状态
我们先来看一个常见的需求,用户列表,可以添加和编辑用户。当用户点击“新建”按钮时,用户可以在输入框中输入新的用户名;单击“编辑”按钮时,编辑后的用户名显示在输入框中,用户可以对其进行修改。当用户单击“确定”按钮时,用户列表会更新。
类UserInput扩展了React。组件{ state={ user : this . props . user } handleChange=(e)={ this . setstate({ user : }).this.state.user,name : e . target . value } });} render(){ const { OnConfirm }=this . props;const { user }=this.state返回(div输入值={user。name | |''} onchange={this。handlechange }/button onclick={()={ on confirm(user)} } OK/button/div);} }类App扩展了React。组件{ state={ users: [ { id: 0,名称: 'bruce' },{ id: 1,名称: 'frank' },{ id: 2,名称: 'tony' } ],Targetuser : { } } OnConfirm=(user)={ const { users }=this . state;const target=users . find(u=u . id===user . id);if(target){ this . SetState({ users 3360[.users.slice(0,users.indexOf(目标)),user,users.slice(users.indexOf(目标)1)]);} else { const id=Math.max(.(users . map(u=u . id)))1;this . SetState({ users 3360[.用户,{ 0.用户,id }]});} } render() { const { users,targetUser }=this.statereturn(div user input user={ TargetUser } OnConfirm={ this . OnConfirm }/ul { users . map(u=(Li键={u.id} {u.name}按钮onClick={()={this。setstate({ targetuser : u })} edit/button/Li()}/ulbutton onclick={()={ this。setstate({ targetuser : } })} create new/button/div)} } react DOM . render(App/,document . getelementbyid(' root '));运行后,效果如下:
现在点击“编辑”和“新建”按钮,输入框中的文字不会切换,因为在点击“编辑”和“更新”时,虽然UserInput的道具发生了变化,但并没有触发状态的更新。因此,需要实现道具变化导致的状态更新,并在UserInput中添加代码:
component willereceiveprops(nextProps){ this . setstate({ user : nextProps . user });}或
static getDerivedStateFromProps(道具,状态){ return { user : props . user };}这样,UserInput每次收到新道具都会自动更新状态。然而,这种实现是有问题的。
派生状态导致的问题
首先,定义了组件的两个概念:受控数据生命和非受控数据生命。受控数据是指通过组件中的道具传入的数据,受父组件影响;不受控制的数据是指完全由组件本身管理的状态,即内部状态。派生状态结合了两种数据源,当这两种数据源发生冲突时,就会出现问题。
问题一
修改用户时,点击“确定”按钮,输入框中的文本变成修改前的文本。比如我把‘布鲁斯’改成了‘李小龙’,确认后输入框改成了‘布鲁斯’,这是我们不想看到的。
出现这个问题的原因是当你点击确定后,App会重新渲染,App会把以前的用户作为道具传递给UserInput。当然,我们可以在每次点击OK后将targetUser重置为一个空对象,但是一旦状态多了,就很难管理了。
问题2
假设页面加载后,一些数据将被异步请求,然后页面将被更新。如果用户在请求页面刷新之前在输入框中输入了一些单词,输入框中的单词将随着页面刷新而被清除。
我们可以向应用程序添加以下代码来模拟异步请求:
componentdimount(){ setTimeout(()={ this . setstate({ text : ' fake request ' })},5000);}出现这个问题的原因是异步请求完成后,App在setState后会重新渲染,每次父组件渲染时都会执行组件的componentWillReceiveProps,而传入的用户是一个空对象,所以UserInput的内容是空的。GetDerivedStateFromProps的调用频率更高,每次渲染组件时都会调用,所以也会出现这个问题。
为了解决这个问题,我们可以在componentWillReceiveProps中判断新进入的用户是否与当前用户相同,如果不相同,则设置状态:
component willereceiveprops(next props){ if(next props . user . id!==this . props . user . id){ this . setstate({ user : nextprops . user });}}更好的解决方案
派生数据源的不确定性会导致各种问题,如果每个数据只由一个组件管理,这些问题是可以避免的。实现这种思想有两种方式,一种是数据完全由父组件管理,另一种是数据完全由组件本身管理。下面分别讨论:
完全受控组件(完全受控组件)
组件的数据完全来自父组件,组件本身不需要管理状态。让我们创建一个完全受控的用户输入版本:
类FullyControlledUserInput扩展了React。组件{ render() { const { user,onConfirm,onChange }=this.props在返回中调用FullyControlledUserInput的方法(div输入值={user。name | | ' ' } onchange={ onchange }/button onclick={()={ onconfirm(user)} } ok/button/div)} } App如下:
.FullyControlledUserInput用户={ TargetUser } OnChange={(e)={ this . SetState({ TargetUser : { id : TargetUser . id,name : e . target . value } });}} onConfirm={this.onConfirm}/.现在FullyControlledUserInput中的所有数据都来自父组件,从而解决了数据冲突和篡改的问题。
完全不受控制的组件(完全不受控制的组件)
组件的数据完全由它自己管理,所以组件中的所有代码都可以被删除,但是传入的道具被保留以设置初始状态值:
类FullyUncontrolledUserInput扩展了React。组件{ state={ user : this . props . user } OnChange=(e)={ this . SetState({ user : }.this.state.user,name : e . target . value } });} render(){ const { user }=this . state;const { onConfirm }=this.props返回(div输入值={user。name | |''} onchange={this。onchange }/button onclick={()={ on confirm(user)} } OK/button/div)} }当传入的道具发生变化时,我们可以通过传入不同的键来重新创建组件的实例App中调用FullyUncontrolledUserInput的方法如下:
Fullyuncontrolled eduser输入用户={targetuser} on confirm={this。on confirm } key={ target user . id }/在大多数情况下,这是一个更好的解决方案。有些人可能认为表演会受到影响。其实性能不会降低多少,如果一个组件的更新逻辑太复杂,也不如重新创建一个新组件来得快。
调用父组件中子组件的方法来设置状态
在某些情况下,如果没有合适的属性作为键,那么随机数或自增数可以作为键传入,或者我们可以定义一种在组件中设置状态的方法,并通过ref将其公开给父组件使用。例如,我们可以添加:
setnewuser state=(新用户)={ this。setstate({ user : new user });}在应用中通过裁判员调用这个方法:
.用户输入用户={ TargetUser } OnConfirm={ this。OnConfirm } ref=' user input '/ul { users。地图(u=(李键={u.id} {u.name}按钮OnClick={()={ this。SetState({ TargetUser : u });这个。参考文献。用户输入。设置新用户状态(u);}} 编辑/button/Li))}/ul button OnClick={()={ this。SetState({ TargetUser : } });这个。参考文献。用户输入。设置新用户状态({ });}} 新建/按钮.这个方法不推荐使用,除非实在没法了。
本文源码请参考:更新方式-道具组件-更换
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。