详解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函数
Oct 16 Javascript
JavaScript中的面向对象介绍
Jun 30 Javascript
jquery ui dialog实现弹窗特效的思路及代码
Aug 03 Javascript
Bootstrap每天必学之缩略图与警示窗
Nov 29 Javascript
jQuery基于$.ajax设置移动端click超时处理方法
May 14 Javascript
javascript中获取元素标签中间的内容的实现方法
Oct 08 Javascript
js获取时间函数及扩展函数的方法
Oct 30 Javascript
详解vue.js数据传递以及数据分发slot
Jan 20 Javascript
微信小程序实现验证码获取倒计时效果
Feb 08 Javascript
Vue  webpack 项目自动打包压缩成zip文件的方法
Jul 24 Javascript
OpenLayers3加载常用控件使用方法详解
Sep 25 Javascript
wepy--用vantUI 实现上弹列表并选择相应的值操作
Nov 03 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
可快速识别放射性物质-国外大神教你diy一个开放式辐射探测器
2020/03/12 无线电
PHP5中使用PDO连接数据库的方法
2010/08/01 PHP
Uncaught exception com_exception with message Failed to create COM object
2012/01/11 PHP
php获取目录所有文件并将结果保存到数组(实例)
2013/10/25 PHP
CodeIgniter基本配置详细介绍
2013/11/12 PHP
百度地图API使用方法详解
2015/08/25 PHP
谈谈php对接芝麻信用踩的坑
2016/12/01 PHP
php与c 实现按行读取文件实例代码
2017/01/03 PHP
laravel + vue实现的数据统计绘图(今天、7天、30天数据)
2018/07/31 PHP
javascript之AJAX框架使用说明
2010/04/24 Javascript
Js控制弹窗实现在任意分辨率下居中显示
2013/08/01 Javascript
Jquery中$.get(),$.post(),$.ajax(),$.getJSON()的用法总结
2013/11/14 Javascript
JavaScript作用域链示例分享
2014/05/27 Javascript
Windows8下搭建Node.js开发环境教程
2014/09/03 Javascript
jQuery使用before()和after()在元素前后添加内容的方法
2015/03/26 Javascript
javascript操作表格排序实例分析
2015/05/06 Javascript
前端性能优化及技巧
2016/05/06 Javascript
使用json来定义函数,在里面可以定义多个函数的实现方法
2016/10/28 Javascript
Bootstrap3 模态框使用实例
2017/02/22 Javascript
Angular表格神器ui-grid应用详解
2017/09/29 Javascript
vue2.0在table中实现全选和反选的示例代码
2017/11/04 Javascript
vue地区选择组件教程详解
2018/05/04 Javascript
解决angularJS中input标签的ng-change事件无效问题
2018/09/13 Javascript
如何修改Vue打包后文件的接口地址配置的方法
2020/04/22 Javascript
Element Notification通知的实现示例
2020/07/27 Javascript
[51:17]Mski vs VGJ.S Supermajor小组赛C组 BO3 第三场 6.3
2018/06/04 DOTA
python爬虫使用cookie登录详解
2017/12/27 Python
为何人工智能(AI)首选Python?读完这篇文章你就知道了(推荐)
2019/04/06 Python
解决python和pycharm安装gmpy2 出现ERROR的问题
2020/08/28 Python
岗位职责定义及内容
2013/11/08 职场文书
大学自荐信
2013/12/12 职场文书
公休请假条
2014/04/11 职场文书
四风问题对照检查材料整改措施
2014/09/27 职场文书
分析ZooKeeper分布式锁的实现
2021/06/30 Java/Android
sass 常用备忘案例详解
2021/09/15 HTML / CSS
yolov5返回坐标的方法实例
2022/03/17 Python