详解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 相关文章推荐
JS的IE和Firefox兼容性集锦
Dec 11 Javascript
推荐自用 Javascript 缩图函数 (onDOMLoaded)……
Oct 23 Javascript
jquery实现图片左右间隔滚动特效(可自动播放)
May 08 Javascript
JSONP跨域的原理解析及其实现介绍
Mar 22 Javascript
JavaScript中的数组操作介绍
Dec 30 Javascript
jQuery Html控件基本操作(日常收集整理)
Mar 11 Javascript
JavaScript组合模式学习要点
Aug 26 Javascript
AngularJS模板加载用法详解
Nov 04 Javascript
JavaScript cookie详解及简单实例应用
Dec 31 Javascript
网页中的图片查看器viewjs使用方法
Jul 11 Javascript
9102了,你还不会移动端真机调试吗
Mar 25 Javascript
vue 监听窗口变化对页面部分元素重新渲染操作
Jul 28 Javascript
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下一个阿拉伯数字转中文数字的函数
2007/07/16 PHP
PHP如何抛出异常处理错误
2011/03/02 PHP
PHP Curl多线程原理实例详解
2013/11/06 PHP
PHP中使用jQuery+Ajax实现分页查询多功能操作(示例讲解)
2017/09/17 PHP
PHP中递归的实现实例详解
2017/11/14 PHP
php 截取中英文混合字符串的方法
2018/05/31 PHP
详解PHP实现支付宝小程序用户授权的工具类
2018/12/25 PHP
DOMAssitant最新版 DOMAssistant 2.5发布
2007/12/25 Javascript
jquery 表单取值常用代码
2009/12/22 Javascript
JavaScript高级程序设计 阅读笔记(十七) js事件
2012/08/14 Javascript
window.navigate 与 window.location.href 的使用区别介绍
2013/09/21 Javascript
jQuery常用操作方法及常用函数总结
2014/06/19 Javascript
document.forms用法示例介绍
2014/06/26 Javascript
fckeditor粘贴Word时弹出窗口取消的方法
2014/10/30 Javascript
js实现延迟加载的方法
2015/06/24 Javascript
浅谈angularJS中的事件
2016/07/12 Javascript
微信小程序 Windows2008 R2服务器配置TLS1.2方法
2016/12/05 Javascript
AngularJS路由实现页面跳转实例
2017/03/03 Javascript
JavaScript中常见的八个陷阱总结
2017/06/28 Javascript
react 国际化的实现代码示例
2018/09/14 Javascript
在vue-cli创建的项目中使用sass操作
2020/08/10 Javascript
Python中线程的MQ消息队列实现以及消息队列的优点解析
2016/06/29 Python
简单谈谈Python中的几种常见的数据类型
2017/02/10 Python
解决matplotlib库show()方法不显示图片的问题
2018/05/24 Python
Linux下远程连接Jupyter+pyspark部署教程
2019/06/21 Python
Python绘制股票移动均线的实例
2019/08/24 Python
浅谈sklearn中predict与predict_proba区别
2020/06/28 Python
使用CSS3的ruby-position固定注音位置的用法示例
2016/07/05 HTML / CSS
HTML5 解析规则分析
2009/08/14 HTML / CSS
Sperry澳大利亚官网:源自美国帆船鞋创始品牌
2019/07/29 全球购物
如果NULL和0作为空指针常数是等价的,那我到底该用哪一个
2014/09/16 面试题
诺思信科技(南京)有限公司.NET笔试题答案
2013/07/06 面试题
四风个人对照检查材料思想汇报
2014/09/25 职场文书
干部个人考察材料
2014/12/24 职场文书
2015年班主任个人工作总结
2015/03/31 职场文书
导游词之扬州大明寺
2019/10/09 职场文书