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

作者:星野时间:2026-10-22 17:48:20浏览:149

本文深入探讨React中当props改变时如何正确更新组件。首先分析使用componentWillReceiveProps或getDerivedStateFromProps管理派生状态(Derived State)时引发的数据冲突与异步请求覆盖问题。随后提出两种更优架构:完全受控组件(数据由父组件管理)与完全不受控组件(利用key重置或ref暴露方法),并对比其适用场景与性能表现,帮助开发者避免常见陷阱。

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'));

运行后的效果如图所示:

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)和不受控数据(uncontrolled 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);
 }

导致这个问题的原因在于,当异步请求完成,setState执行后,App会重新渲染。此时,组件的componentWillReceiveProps会在父组件每次渲染时执行,而传入的user是一个空对象,导致UserInput的内容被清空。由于getDerivedStateFromProps调用频率更高,会在组件每次渲染时执行,因此也会产生该问题。

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


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

更优的组件架构方案

更好的解决方案

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

完全受控组件实现

完全受控组件(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}
/>

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

通过Ref暴露子组件方法

在父组件中调用子组件的方法设置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

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

作者最新文章
相关文章
更多