视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
详解React中传入组件的props改变时更新组件的几种实现方法
2020-11-27 22:07:51 责编:小采
文档


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

下载本文
显示全文
专题