详解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判断一个元素是否为另一个元素的子元素的代码
Mar 21 Javascript
原生Js实现元素渐隐/渐现(原理为修改元素的css透明度)
Jun 24 Javascript
jquery解析xml字符串简单示例
Apr 11 Javascript
JavaScript更改原始对象valueOf的方法
Mar 19 Javascript
js实现的全国省市二级联动下拉选择菜单完整实例
Aug 17 Javascript
微信小程序 WXML、WXSS 和JS介绍及详解
Oct 08 Javascript
AngularJS实现动态编译添加到dom中的方法
Nov 04 Javascript
BootStrap实现带关闭按钮功能
Feb 15 Javascript
canvas绘制一个常用的emoji表情
Mar 30 Javascript
JS实现的计数排序与基数排序算法示例
Dec 04 Javascript
Element 默认勾选表格 toggleRowSelection的实现
Sep 04 Javascript
详解Vite的新体验
Feb 22 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 spl_autoload_register实现自动加载研究
2011/12/06 PHP
PHP中的socket_read和socket_recv区别详解
2015/02/09 PHP
一个不错的应用,用于提交获取文章内容,不推荐用
2007/03/03 Javascript
JSONP跨域的原理解析及其实现介绍
2014/03/22 Javascript
超级简单实现JavaScript MVC 样式框架
2015/03/24 Javascript
jQuery判断指定id的对象是否存在的方法
2015/05/22 Javascript
javascript实现给定半径求出圆的面积
2015/06/26 Javascript
jquery简单倒计时实现方法
2015/12/18 Javascript
Bootstrap模块dropdown实现下拉框响应
2016/05/22 Javascript
Bootstrap表单制作代码
2017/03/17 Javascript
jquery版轮播图效果和extend扩展
2017/07/18 jQuery
微信小程序使用toast消息对话框提示用户忘记输入用户名或密码功能【附源码下载】
2017/12/09 Javascript
JavaScript通过mouseover()实现图片变大效果的示例
2017/12/20 Javascript
JavaScript中的高级函数
2018/01/04 Javascript
JavaScript实现图片上传并预览并提交ajax
2019/09/30 Javascript
Node.js利用Express实现用户注册登陆功能(推荐)
2020/10/26 Javascript
[01:05:56]2018DOTA2亚洲邀请赛3月29日 小组赛A组 Newbee VS VG
2018/03/30 DOTA
Python实现爬虫设置代理IP和伪装成浏览器的方法分享
2018/05/07 Python
Python实现的redis分布式锁功能示例
2018/05/29 Python
关于Pycharm无法debug问题的总结
2019/01/19 Python
Django项目后台不挂断运行的方法
2019/08/31 Python
Python tkinter实现图片标注功能(完整代码)
2019/12/08 Python
基于Django实现日志记录报错信息
2019/12/17 Python
pytorch 状态字典:state_dict使用详解
2020/01/17 Python
python实现可下载音乐的音乐播放器
2020/02/25 Python
Django values()和value_list()的使用
2020/03/31 Python
python 使用openpyxl读取excel数据
2021/02/18 Python
使用css3实现的windows8开机加载动画
2014/12/09 HTML / CSS
APM Monaco中国官网:来自摩纳哥珠宝品牌
2017/12/27 全球购物
个人评价范文分享
2014/01/11 职场文书
安全生产责任书
2014/03/12 职场文书
十佳文明家庭事迹
2014/05/25 职场文书
小学生关于梦想的演讲稿
2014/08/22 职场文书
单位车辆管理制度
2015/08/05 职场文书
Python基础知识学习之类的继承
2021/05/31 Python
java设计模式--建造者模式详解
2021/07/21 Java/Android