我们使用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); }
导致这个问题的原因在于,当异步请求完成,setState后App会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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持小牛知识库。
本文向大家介绍在React中组件的props改变时更新组件的有哪些方法?相关面试题,主要包含被问及在React中组件的props改变时更新组件的有哪些方法?时的应答技巧和注意事项,需要的朋友参考一下 新版用 getDerivedstatefromProps(nextProps){ 业务逻辑}
本文向大家介绍react 组件传值的三种方法,包括了react 组件传值的三种方法的使用技巧和注意事项,需要的朋友参考一下 整理 react 组件传值 三种方式 父组件向子组件传值(通过props传值) 子组件: 父组件: 父组件向子组件传值(回调函数) 子组件 父组件 兄弟组件传值(子传给父,父再传给另一个子) 子组件1 子组件2 父组件 以上就是本文的全部内容,希望对大家的学习有所帮助,也希望
本文向大家介绍vue组件中使用props传递数据的实例详解,包括了vue组件中使用props传递数据的实例详解的使用技巧和注意事项,需要的朋友参考一下 在 Vue 中,父子组件的关系可以总结为 props向下传递,事件向上传递。父组件通过 props 给子组件下发数据,子组件通过事件给父组件发送消息。看看它们是怎么工作的。 一、基本用法 组件不仅仅是要把模板的内容进行复用,更重要的是组件间要进行
本文向大家介绍Vue.js组件props数据验证实现详解,包括了Vue.js组件props数据验证实现详解的使用技巧和注意事项,需要的朋友参考一下 本文实例为大家分享了Vue.js组件props数据验证的具体代码,供大家参考,具体内容如下 数据验证 一般当组件需要提供给别人使用时,就需要使用数据验证。 示例: 验证的type类型可以是: String Number Boolean Object A
我对JS的反应有点陌生。我有两个问题。我来了 现在我要做的是,当应用程序组件挂载时,我生成一个对后端服务器的ajax调用,当它返回时,它更新道具,并设置状态,这样子组件也会重新呈现,但子组件不会重新呈现。谁能告诉我出了什么问题。 问题2 现在让我们假设我将进入组件二路由,它将呈现组件二,我在应用程序组件中生成一个ajax调用,根据ajax调用返回的数据,我在应用程序组件中设置了一些道具,我还希望组
本文向大家介绍vue的props实现子组件随父组件一起变化,包括了vue的props实现子组件随父组件一起变化的使用技巧和注意事项,需要的朋友参考一下 本文实例为大家分享了vue的props实现父组件变化子组件一起变化,供大家参考,具体内容如下 类似于用 v-bind 绑定 HTML 特性到一个表达式,也可以用 v-bind 绑定动态 Props 到父组件的数据。每当父组件的数据变化时,也会传导给