专栏首页MudOnTireReact中传入组件的props改变时更新组件的几种实现方法

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

我们使用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'));

运行后,效果如图:

现在点击‘编辑’和‘新建’按钮,输入框中的文字并不会切换,因为点击‘编辑’和‘更新’时,虽然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’,这是我们不愿意看到的。

出现这个问题的原因是,点击确定,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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 前端需要了解的9种设计模式 什么是设计模式?设计模式的类型一. 结构型模式(Structural Patterns)二. 创建型模式(Creat

    设计模式是对软件设计开发过程中反复出现的某类问题的通用解决方案。设计模式更多的是指导思想和方法论,而不是现成的代码,当然每种设计模式都有每种语言中的具体实现方式...

    MudOnTire
  • 每天一个小技巧:Javascript中定义私有属性(Private Properties) IIFE 实现构造函数实现Class实现原生实现

    和很多高级语言不同,JavaScript 中没有 public、private、protected 这些访问修饰符(access modifiers),而且长期...

    MudOnTire
  • 每天一个小技巧:实现自定义右键菜单(Context Menu) contextmenu 事件监听构造菜单显示菜单隐藏菜单

    首先,我们需要禁用浏览器弹出默认菜单的行为,通过阻止 contextMenu 事件的默认行为,并同时触发自定义菜单的显示:

    MudOnTire
  • 微信小程序-音乐播放器+背景播放

    1.正常播放音频 2.可以滑动进度条 3.可以切换上一条,下一条音频 4.退出当前页或关闭小程序之后仍然可以正常播放 5.试听功能进入该播放页不可以播放上一条,...

    super.x
  • 从 0 到 1 实现 React 系列 —— 组件和 state|props

    看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (x)react 的同时理顺 React 框架的主干内容(JSX/虚拟DOM/组件/...)

    牧云云
  • react --- React中state和props分别是什么?

    组件从概念上看就是一个函数,可以接受一个参数作为输入值,这个参数就是props,所以可以把props理解为从外部传入组件内部的数据。由于React是单向数据流,...

    小蔚
  • React父子组件传值

    版权声明:原创不易,多多珍惜 h...

    我乃小神神
  • React学习(五)-React中组件的数据-props

    开发一个React应用,更多的是在编写组件,而React组件最小的单位就是React元素,编写组件的最大的好处,就是实现代码的复用

    itclanCoder
  • React基础(5)-React中组件的数据-props

    开发一个React应用,更多的是在编写组件,而React组件最小的单位就是React元素,编写组件的最大的好处,就是实现代码的复用

    itclanCoder
  • React中的State与Props

    一个组件的显示形态可以由数据状态和外部参数决定,其中,数据状态为 state,外部参数为 props

    Leophen

扫码关注云+社区

领取腾讯云代金券