详解React中传入组件的props改变时更新组件的几种实现方法


Posted in Javascript onSeptember 13, 2018

我们使用react的时候常常需要在一个组件传入的props更新时重新渲染该组件,常用的方法是在componentWillReceiveProps中将新的props更新到组件的state中(这种state被成为派生状态(Derived State)),从而实现重新渲染。React 16.3中还引入了一个新的钩子函数getDerivedStateFromProps来专门实现这一需求。但无论是用componentWillReceiveProps还是getDerivedStateFromProps都不是那么优雅,而且容易出错。所以今天来探讨一下这类实现会产生的问题和更好的实现方案。

何时使用派生状态

咱们先来看一个比较常见的需求,一个用户列表,可以新增和编辑用户,当用户点击‘新建'
按钮用户可以在输入框中输入新的用户名;当点击‘编辑'按钮的时候,输入框中显示被编辑的用户名,用户可以修改;当用户点击‘确定'按钮的时候用户列表更新。

class UserInput extends React.Component {

 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;
  return (
   <div>
    <input value={user.name || ''} onChange={this.handleChange} />
    <button onClick={() => { onConfirm(user) }}>确定</button>
   </div>
  );
 }
}

class App extends React.Component {
 state = {
  users: [
   { id: 0, name: 'bruce' },
   { id: 1, name: 'frank' },
   { id: 2, name: 'tony' }
  ],
  targetUser: {}
 }

 onConfirm = (user) => {
  const { users } = this.state;
  const target = users.find(u => u.id === user.id);

  if (target) {
   this.setState({
    users: [
     ...users.slice(0, users.indexOf(target)),
     user,
     ...users.slice(users.indexOf(target) + 1)
    ]
   });
  } else {
   const id = Math.max(...(users.map(u => u.id))) + 1;
   this.setState({
    users: [
     ...users,
     {
      ...user,
      id
     }
    ]
   });
  }
 }

 render() {
  const { users, targetUser } = this.state;
  return (
   <div>
    <UserInput user={targetUser} onConfirm={this.onConfirm} />
    <ul>
     {
      users.map(u => (
       <li key={u.id}>
        {u.name}
        <button onClick={() => { this.setState({ targetUser: u }) }}>编辑</button>
       </li>
      ))
     }
    </ul>
    <button onClick={() => { this.setState({ targetUser: {} }) }}>新建</button>
   </div>
  )
 }
}

ReactDOM.render(<App />, document.getElementById('root'));

运行后,效果如图:

详解React中传入组件的props改变时更新组件的几种实现方法

现在点击‘编辑'和‘新建'按钮,输入框中的文字并不会切换,因为点击‘编辑'和‘更新'时,虽然UserInput的props改变了但是并没有触发state的更新。所以需要实现props改变引发state更新,在UserInput中增加代码:

componentWillReceiveProps(nextProps) {
  this.setState({
   user: nextProps.user
  });
 }

或者

static getDerivedStateFromProps(props, state) {
  return {
   user: props.user
  };
 }

这样就实现了UserInput每次接收新的props的时候自动更新state。但是这种实现方式是有问题的。

派生状态导致的问题

首先来明确组件的两个概念:受控数据(controlled data lives)和不受控数据(uncontrollered data lives)。受控数据指的是组件中通过props传入的数据,受到父组件的影响;不受控数据指的是完全由组件自己管理的状态,即内部状态(internal state)。而派生状态揉合了两种数据源,当两种数据源产生冲突时,问题随之产生。

问题一

当在修改一个用户的时候,点击‘确定'按钮,输入框里的文字又变成了修改之前的文字。比如我将‘bruce'修改为‘bruce lee',确定后,输入框中又变成了‘bruce',这是我们不愿意看到的。

详解React中传入组件的props改变时更新组件的几种实现方法

出现这个问题的原因是,点击确定,App会re-render,App又将之前的user作为props传递给了UserInput。我们当然可以在每次点击确定之后将targetUser重置为一个空对象,但是一旦状态多了之后,这样管理起来非常吃力。

问题二

假设页面加载完成后,会异步请求一些数据然后更新页面,如果用户在请求完成页面刷新之前已经在输入框中输入了一些文字,随着页面的刷新输入框中的文字会被清除。

我们可以在App中加入如下代码模拟一个异步请求:

componentDidMount() {
  setTimeout(() => {
   this.setState({
    text: 'fake request'
   })
  }, 5000);
 }

导致这个问题的原因在于,当异步请求完成,setStateApp会re-render,而组件的componentWillReceiveProps会在父组件每次render的时候执行,而此时传入的user是一个空对象,所以UserInput的内容被清空了。而getDerivedStateFromProps调用的更频繁,会在组件每次render的时候调用,所以也会产生该问题。

为了解决这个问题我们可以在componentWillReceiveProps中判断新传入的user和当前的user是否一样,如果不一样才设置state:

componentWillReceiveProps(nextProps) {
  if (nextProps.user.id !== this.props.user.id) {
   this.setState({
    user: nextProps.user
   });
  }
 }

更好的解决方案

派生状态的数据源的不确定性会导致各种问题,那如果每份数据有且只被一个component管理应该就能避免这些问题了。这种思路有两种实现,一种是数据完全由父组件管理,一种是数据完全由组件自己管理。下面分别讨论:

完全受控组件(fully controlled component)

组件的数据完全来自于父组件,组件自己将不需要管理state。我们新建一个完全受控版的UserInput

class FullyControlledUserInput extends React.Component {
 render() {
  const { user, onConfirm, onChange } = this.props;
  return (
   <div>
    <input value={user.name || ''} onChange={onChange} />
    <button onClick={() => { onConfirm(user) }}>确定</button>
   </div>
  )
 }
}

App中调用FullyControlledUserInput的方法如下:

...
  <FullyControlledUserInput
   user={targetUser}
   onChange={(e) => {
    this.setState({
     targetUser: {
      id: targetUser.id,
      name: e.target.value
     }
    });
   }}
   onConfirm={this.onConfirm}
  />
...

现在FullyControlledUserInput中的所有的数据都来源于父组件,由此解决数据冲突和被篡改的问题。

完全不受控组件(fully uncontrolled component)

组件的数据完全由自己管理,因此componentWillReceiveProps中的代码都可以移除,但保留传入props来设置state初始值:

class FullyUncontrolledUserInput extends React.Component {
 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;
  return (
   <div>
    <input value={user.name || ''} onChange={this.onChange} />
    <button onClick={() => { onConfirm(user) }}>确定</button>
   </div>
  )
 }
}

当传入的props发生改变时,我们可以通过传入一个不一样的key来重新创建一个component的实例来实现页面的更新。App中调用FullyUncontrolledUserInput的方法如下::

<FullyUncontrolledUserInput
 user={targetUser}
 onConfirm={this.onConfirm}
 key={targetUser.id}
/>

大部分情况下,这是更好的解决方案。或许有人会觉得这样性能会受影响,其实性能并不会变慢多少,而且如果组件的更新逻辑过于复杂的话,还不如重新创建一个新的组件来的快。

在父组件中调用子组件的方法设置state

如果某些情况下没有合适的属性作为key,那么可以传入一个随机数或者自增的数字作为key,或者我们可以在组件中定义一个设置state的方法并通过ref暴露给父组件使用,比如我们可以在UserInput中添加:

setNewUserState = (newUser) => {
  this.setState({
   user: newUser
  });
 }

在App中通过ref调用这个方法:

...
  
  <UserInput user={targetUser} onConfirm={this.onConfirm} ref='userInput' />
   <ul>
   {
    users.map(u => (
     <li key={u.id}>
      {u.name}
      <button onClick={() => {
       this.setState({ targetUser: u });
       this.refs.userInput.setNewUserState(u);
      }}>
       编辑
      </button>
     </li>
    ))
   }
  </ul>
  <button onClick={() => {
   this.setState({ targetUser: {} });
   this.refs.userInput.setNewUserState({});
  }}>
   新建
  </button>
  
  ...

这个方法不推荐使用,除非实在没法了。。

本文源码请参考:ways-to-update-component-on-props-change

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Javascript &amp; DHTML 实例编程(教程)(三)初级实例篇1—上传文件控件实例
Jun 02 Javascript
下载网站打开页面后间隔多少时间才显示下载链接地址的代码
Apr 25 Javascript
jQuery学习笔记之jQuery的动画
Dec 22 Javascript
使用js简单实现了tree树菜单
Nov 20 Javascript
Eclipse去除js(JavaScript)验证错误
Feb 11 Javascript
jQuery中:file选择器用法实例
Jan 04 Javascript
使用Ajax生成的Excel文件并下载的实例
Nov 21 Javascript
JS作用域闭包、预解释和this关键字综合实例解析
Dec 16 Javascript
深入理解javascript中的 “this”
Jan 17 Javascript
webpack实践之DLLPlugin 和 DLLReferencePlugin的使用教程
Jun 10 Javascript
js实现整体缩放页面适配移动端
Mar 31 Javascript
Vue router配置与使用分析讲解
Dec 24 Vue.js
vue 刷新之后 嵌套路由不变 重新渲染页面的方法
Sep 13 #Javascript
解决vuejs项目里css引用背景图片不能显示的问题
Sep 13 #Javascript
Vue-不允许嵌套式的渲染方法
Sep 13 #Javascript
通过vue-cli3构建一个SSR应用程序的方法
Sep 13 #Javascript
vue.js单文件组件中非父子组件的传值实例
Sep 13 #Javascript
JavaScript数组方法的错误使用例子
Sep 13 #Javascript
vue仿element实现分页器效果
Sep 13 #Javascript
You might like
解析PHP实现下载文件的两种方法
2013/07/05 PHP
详解Window7 下开发php扩展
2015/12/31 PHP
php 如何设置一个严格控制过期时间的session
2017/05/05 PHP
详解PHP使用日期时间处理器Carbon人性化显示时间
2017/08/10 PHP
JavaScript CSS修改学习第六章 拖拽
2010/02/19 Javascript
ASP.NET jQuery 实例15 通过控件CustomValidator验证CheckBoxList
2012/02/03 Javascript
浅谈nodeName,nodeValue,nodeType,typeof 的区别
2015/01/13 Javascript
jQuery实现的手机发送验证码倒计时效果代码分享
2015/08/24 Javascript
jQuery模仿阿里云购买服务器选择购买时间长度的代码
2016/04/29 Javascript
使用BootStrap实现用户登录界面UI
2016/08/10 Javascript
Javascript 动态改变imput type属性
2016/11/01 Javascript
javascript构造函数以及原型对象的理解
2017/01/13 Javascript
Bootstrap fileinput文件上传组件使用详解
2017/06/06 Javascript
JS全角与半角转化实例(分享)
2017/07/04 Javascript
基于iview的router常用控制方式
2019/05/30 Javascript
javascript实现简单打字游戏
2019/10/29 Javascript
[02:03]《现实生活中的DOTA2》—林书豪&DOTA2职业选手出演短片
2015/08/18 DOTA
wxpython 学习笔记 第一天
2009/02/09 Python
Python函数参数操作详解
2018/08/03 Python
python进阶之多线程对同一个全局变量的处理方法
2018/11/09 Python
Python使用MyQR制作专属动态彩色二维码功能
2019/06/04 Python
华为2019校招笔试题之处理字符串(python版)
2019/06/25 Python
Tensorflow tf.nn.atrous_conv2d如何实现空洞卷积的
2020/04/20 Python
Python容器类型公共方法总结
2020/08/19 Python
发现两个有趣的CSS3动画效果
2013/08/14 HTML / CSS
HTML5轻松实现全屏视频背景的示例
2018/04/23 HTML / CSS
Agoda香港:全球特价酒店预订
2017/05/07 全球购物
英国和世界各地鲜花速递专家:Arena Flowers
2018/02/10 全球购物
嘻哈珠宝品牌:KRKC&CO
2020/10/19 全球购物
大一新生期末自我评价
2014/09/12 职场文书
2015年全国科普日活动总结
2015/03/23 职场文书
2015人事行政工作总结范文
2015/05/21 职场文书
高考百日冲刺决心书
2015/09/23 职场文书
2016年安全生产先进个人事迹材料
2016/02/29 职场文书
k-means & DBSCAN 总结
2021/04/27 Python
Spring Boot 整合 Apache Dubbo的示例代码
2021/07/04 Java/Android