原文链接:https://reactjs.org/blog/2018...
React 16.4包含了一个getDerivedStateFromProps的 bug 修复:曾带来一些 React 组件频繁复现的 已有bug。如果你的应用曾经采用某种反模式写法,但是在这次修复之后没有被覆盖到你的情况,我们对于该 bug 深感抱歉。在下文,我们会阐述一些常见的,derived state相关的反模式,还有我们的建议写法。

很长一段时间,componentWillReceiveProps是响应props 改变,不会带来额外重新渲染,更新 state 的唯一方式。在16.3版本中,我们引入了一个生命周期方法getDerivedStateFromProps,为的是以一种更安全的方式来解决同样的问题。同时,我们意识到人们对于这两个钩子函数的使用有许多误解,也发现了一些造成这些晦涩 bug 的反模式。getDerivedStateFromProps的16.4版本修复使得 derived state更稳定,滥用情况会减少一些。

注意事项

本文提及的所有反模式案例面向旧钩子函数componentWillReceiveProps和新钩子函数getDerivedStateFromProps

本文会涵盖下面讨论:

  • 什么时候去使用 derived state
  • 一些 derived state 的常见 bug

    • 反模式:无条件地拷贝props 到state
    • 反模式:当 props 改变的时候清除 state
  • 建议解决方案
  • 内存化

什么时候去使用Derived State

getDerivedStateFromProps存在的唯一目的是使得组件在 props 改变时能都更新好内在state。我们之前的博文有过一些例子,比如基于一个变化着的偏移 prop 来记录当前滚动方向或者根据一个来源 prop 来加载外部数据。

我们没有给出许多例子,因为总体原则上来讲,derived state 应该用少点。我们见过的所有derived state 的问题大多数可以归结为,要么没有任何前提条件的从 props 更新state,要么 props,state 不匹配的任何时候去更新 state。(我们将在下面谈及更多细节)

  • 如果你正在使用 derived state 来进行一些基于当前 props 的内存化计算,那么你不需要 derived state。memoization 小节会细细道来。
  • 如果你在无条件地更新 derived state或者 props,state 不匹配的时候去更新它,你的组件很可能太频繁地重置 state,继续阅读可见分晓。

derived state 的常见 bug

受控,不受控概念通常针对表单输入,但是也可以用来描述组件的数据活动。props 传递进来的数据可以看成受控的(因为父组件控制了数据源)。组件内部状态的数据可以看成不受控的(因为组件能直接改变他)。

最常见的derived state错误 就是混淆两者(受控,不受控数据);当一个 state 的变更字段也可以通过 setState 调用来更新的时候,就没有一个单一的(真相)数据源。上面谈及的加载外部数据的例子可能听起来情况类似,但是一些重要方面还是不一样的。在加载例子中,source 属性和 loading 状态有着一个清晰数据源。当source prop改变的时候,loading 状态总是被重写。相反,loading 状态只会在 prop 改变的时候被重写,其他情况下就是被组件管控着。

问题就是在这些约束变化的时候出现的。最典型的两种形式如下,我们来瞧瞧:

反模式: 无条件的从 props 拷贝至 state

一个常见的误解就是以为getDerivedStateFromPropscomponentWillReceivedProps会只在props 改变的时候被调用。实际上这两个钩子函数可能在父组件渲染的任何时候被调用,不管 props 是不是和以前不同。因此,用这两个钩子函数来无条件消除 state 是不安全的。这样做会使得 state 更新丢失。

我们看看一个范例,这是一个邮箱输入组件,镜像了一个 email prop 到 state:

class EmailInput extends Component {state = { email: this.props.email }render () {return <input onChange={this.handleChange} value={this.state.email} />}handleChange = e => {this.setState({ email: e.target.value })}componentWillReceiveProps(nextProps) {// This will erase any local state updates!// Do not do this.this.setState({ email: nextProps.email })}
}

刚开始,该组件可能看起来 Okay。State 依靠 props 来进行值初始化,我们输入的时候也会更新 State。但是如果父组件重新渲染的时候,我们敲入的任何字符都会被忽略。就算我们在 钩子函数setState 之前进行了nextProps.email !== this.state.email的比较,也无济于事。

在这个简单例子中,我们可以通过增加shouldComponentUpdate,使得只在 email prop改变的时候重新渲染。但是实践表明,组件通常会有多个 prop,另一个 prop的改变仍旧可能造成重新渲染还是有不正确的重置。函数和对象类型的 prop 经常行内生成。使得shouldComponentUpdate只允许在一种情形发生时返回 true很难实现。这儿有个直观例子。所以,shouldComponentUpdate是性能优化的最佳手段,不要想着确保 derived state 的正确使用。

希望现在的你明白了为什么无条件拷贝 props 到 state 是个坏主意。在总结解决方案之前,我们来看看相关反模式:如果我们指向在 email prop 改变的时候去更新 state 呢

反模式: props 改变的时候擦除 state
接着上面例子继续,我们可以避免在 props.email改变的时候故意擦除 state:

class EmailInput extends Component {state = {email: this.props.email}componentWillReceiveProps(nextProps) {// Any time props.email changes, update state.if (nextProps.email !== this.props.email) {this.setState({email: nextProps.email})}}
}

注意事项

即使上面的例子中只谈到 componentWillReceiveProps, 但是也同样适用于getDerivedStateFromProps

我们已经改善许多,现在组件会只在props 改变的时候清除我们输入过的旧字符。

但是还有一个残留问题。想象一下一个密码控件在使用上述输入框组件,当涉及到拥有同一邮箱的两个帐号的细节式,输入框无法重置。因为 传递给组件的prop值,对于两个帐号而言是一样的。这会困扰到用户,因为一个账号还没保存的变更将会影响到共享同一邮箱的其他帐号。这有demo。

这是个根本性的设计失误,但是也很容易犯错,比如我。幸运的是有两个更好的方案。关键在于,对于任何片段数据,需要用一个单独组件来保存数据,并且要避免在其他组件重复。我们来看看这两个方案:

解决方案

推荐方案一:全受控组件

避免上面问题的一个办法,就是从组件当中完全移除 state。如果我们的邮箱地址只是作为一个 prop 存在,那么我们不用担心和 state 的冲突。甚至可以把EmailInput转换成一个更轻量的函数组件:

function EmailInput(props) {return <input onChange={props.onChange} value={props.email} />
}

这个办法简化了组件的实现,如果我们仍然想要保存草稿值的话,父表单组件将需要手动处理。这有一个这种模式的demo。

推荐方案二: 带有 key 属性的全不受控组件

另一个方案就是我们的组件需要完全控制 draft 邮箱状态值。这样的话,组件仍然可以接受一个prop初始值,但是会忽略该prop 的连续变化:

class EmailInput extends Component {state = { email: this.props.defaultEmail }handleChange = e => {this.setState({ email: e.target.value })}render () {return <input onChange={this.handleChange} value={this.state.email} />}
}

在聚焦到另一个表单项的时候为了重置邮箱值(比如密码控件场景),我们可以使用React 的 key 属性。当 key 变化时,React 会创建一个新组件实例,而不是更新当前组件。Keys 通常对于动态列表很有用,不过在这里也很有用。在一个新用户选中时,我们用 user ID 来重新创建一个表单输入框:

<EmailInputdefaultEmail={this.props.user.email}key={this.props.user.id}
/>

每次 ID 改变的时候,EmailInput输入框都会重新生成,它的 state 也就会重置到最新的 defaultEmail值。栗子不能少,这个方案下,没有必要把 key 值添加到每个输入框。在整个form表单上 添加一个 key 属性或许会更合理。每次 key 变化时,表单内的所有组件都会重新生成,同时初始化 state。

在大多数情况,这是处理需要重置的state的最佳办法。

注意事项

这个办法可能听起来性能慢,但是实际表现上可能微不足道。如果一个组件有复杂更新逻辑的话使用key属性可能会更快,因为diffing算法走了弯路

  • 方案一:通过 ID 属性重置 uncontrolled 组件

如果 key 由于某个原因不生效(有可能是组件初始化成本高),那么一个可用但是笨拙的办法就是在getDerivedStateFromProps里监听userID 的变化。

class EmailInput extends Component {state = {email: this.props.defaulEmail,pervPropsUserID: this.props.userID,}static getDerivedFromProps(nextProps, prevState) {// Any time the current user changes,// Reset any parts of state that are tied to that user.// In this simple example, that's just the email.if (nextProps.userID !== prevState.prevPropsUserID) {return {prevPropsUserID: nextProps.userID,email: nextProps.defaultEmail,}}return null}// ...
}

如果这么做的话,也给只重置组件部分内在状态带来了灵活性,举个例子。

注意事项

即使上面的例子中只谈到 getDerivedStateFromProps, 但是也同样适用于componentWillReceiveProps

  • 方案二:用实例方法来重置非受控组件

极少情况下,即使没有用作 key 的合适 ID,你还是想重置 state。一个办法是把 key重置成随机值或者每次你想重置的时候会自动纠正。另一个选择就是用一个实例方法用来命令式地重置内部状态。

class EmailInput extends Component {state = {email: this.props.defaultEmail,}resetEmailForNewUser (newEmail) {this.setState({ email: newEmail })}// ...
}

父表单组件就可以使用一个 ref 属性来调用这个方法,这里有 Demo.

总结

总结一下,设计一个组件的时候,重要的是确定数据是受控还是不受控。

不要把 prop 值“镜像”到 state,而是要让组件受控,并且合并在一些父组件中的两个分叉值。比如说,不是要让子组件接收一个props.value,并且跟踪一个草稿字段state.value,而是要让父组件管理 state.draftValue还有state.committedValue,直接控制子组件的值。会使得数据流更明显,更稳定。

对于不受控组件,如果你想要在一个 ID 这样的特殊 prop 变化的时候重置 state,你会有以下选项:

  • 推荐:为了重置所有内部state,使用 key 属性
  • 方案一:为了重置某些字段值,监听一个props.userID这种特殊字段的变化
  • 方案二:也可以会退到使用 refs 属性的命令式实例方法

内存化

我们已经看到 derived state 为了确保一个用在 render的字段而在输入框变化时被重新计算。这项技术叫做内存化

使用 derived state 去达到内存化并没有那么糟糕,但是也不是最佳方案。管理 derived state 本身比较复杂,属性变多时变得更复杂了。比如说,如果我们增加第二个 derived 字段到我们的组件 state,那么我们需要针对两个值的变化来做追踪。

看看一个组件例子,它有一个列表 prop,组件渲染出匹配用户查询输入字符的列表选项。我们应该使用 derived state 来存储过滤好的列表。

class Example extends Component {state = {filterText: '',}// ********************// NOTE: this example is NOT the recommended approach.// See the examples below for our recommendations instead.// ********************staitic getDerivedStateFromProps(nextProps, prevState) {// Re-run the filter whenever the list array or filter text change.// Note we need to store prePropsList and prevFilterText to detect change.if ( nextProps.list !== prevState.prevPropsList || prevState.prevFilterList !== prevState.filterText) {return {prevPropsList: nextProps.list,prevFilterText: prevState.filterText,filteredList: nextProps.list.filter(item => item.text.includes(prevState.filterText))}}return null}handleChange = e => {this.setState({ filterText: e.target.value })}render () {return (<Fragment><input onChange={this.handleChange} value={this.state.filterText} /><ul>{this.state.filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul></Fragment>)}
}

该实现避免了filteredList经常不必要的重新计算。但是也复杂了些。因为需要单独追踪 props和 state 的变化,为的是适当的更新过滤好的列表。这里,我们可以使用PureCompoennt来做简化,把过滤操作放到 render 方法里去:

// PureCompoents only rerender if at least one stae or prop value changes.
// Change is determined by doing a shallow comparison of stae and prop keys.
class Example Extends PureComponent {// State only needs to hold the current filter text value:state = {filterText: '',}handleChange = e => {htis.setState({ filterText: e.target.value })}render () {// The render method on this PureComponent is called only if// props.list or state.filterList has changed.const filteredList = this.props.list.filter(item => item.text.includes(this.stae.filterText))return (<Fragment><input onChange={this.handleChange} value={this.state.filterText} /><ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul></Fragment>)}
}

上面代码要干净多了而且比 derived state 版本要更简单。只是偶尔不够好:对于大列表的过滤有点慢,而且如果另一个 prop 要变化的话PureComponent不会防止重新渲染。基于这样的考虑,我们增加了memoization helper来避免非必要的列表重新过滤:

import memoize from 'memoize-one'class Example extends Component {// State only need to hold the current filter text value:state = { filterText: '' }filter = memoize((list, filterText) => list.filter(item => item.text.includes(filterText)))handleChange = e => {this.setState({ filterText: e.target.value })}render () {// Calculate the latest filtered list. If these arguments havent changed// since the last render, `'memoize-one` will reuse the last return value.const filteredList = this.filter(this.props.list, this.sate.filterText)return (<Fragment><input onChange={this.handleChange} value={this.state.filterText} /><ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul></Fragment>)}
}

这要简单多了,而且和 derived state 版本一样好。

当使用memoization的时候,需要满足一些条件:

  1. 在大多数情况下,你会把内存化函数添加到一个组件实例上。这会防止该组件的多个实例重置每一个内存化属性。
  2. 通常你使用一个带有有限缓存大小的内存化工具,为的是防止时间累计下来的内存泄露。(在上述例子中,我们使用memoize-one因为它仅仅会缓存最近的参数和结果)。
  3. 这一节里,如果每次父组件渲染的时候props.list重新生成的话,上述实现会失效。但是在多数情况下,上述实现是合适的。

结束语

在实际应用中,组件经常混合着受控和不受控的行为。理所应当。如果每个值都有明确源,你就可以避免上面的反模式。

重申一下,由于比较复杂,getDerivedStateFromProps(还有 derived state)是一项高级特性,而且应该用少点。如果你使用的时候遇到麻烦,请在 GitHub 或者 Twitter 上联系我们。

You Probably Dont Need Derived State相关推荐

  1. react引入多个图片_重新引入React:v16之后的每个React更新都已揭开神秘面纱。

    react引入多个图片 In this article (and accompanying book), unlike any you may have come across before, I w ...

  2. Designing Data-Intensive Applications

    寻找翻译本书后续章节合作者  微信:18600166191 ----------------------------------- Designing Data-Intensive Applicati ...

  3. 牛散村:国内低代码平台有哪些?low code平台整理分享!

    全栈平台 阿里-云凤蝶 蚂蚁杨周璇:我做前端这十多年来的感悟 云凤蝶可视化搭建的推导与实现 云凤蝶中台研发提效实践 中台建站的智能化探索 云凤蝶如何打造媲美 sketch 的自由画布 云凤蝶自由画布之 ...

  4. 前端面试 React篇(上)

    一.组件基础 1. React 事件机制 <div onClick={this.handleClick.bind(this)}>点我</div> React并不是将click事 ...

  5. React 性能优化完全指南,将自己这几年的心血总结成这篇!

    作者: MoonBall 原文地址: https://juejin.cn/post/6935584878071119885 本文分为三部分,首先介绍 React 的工作流,让读者对 React 组件更 ...

  6. Designing Data-Intensive Applications(设计数据密集应用)- O'Reilly 2017 读书笔记

    Designing Data-Intensive Applications The Big Ideas Behind Reliable, Scalable, and Maintainable Syst ...

  7. Hooks 与 React 生命周期

    一.Hooks 组件 函数组件 的本质是函数,没有 state 的概念的,因此不存在生命周期一说,仅仅是一个 render 函数而已. 但是引入 Hooks 之后就变得不同了,它能让组件在不使用 cl ...

  8. 【翻译】What is State Machine Diagram(什么是状态机图)?

    [翻译]What is State Machine Diagram(什么是状态机图)? 写在前面 在上一篇学习类图的时候将这个网站上的类图的一篇文章翻译了出来,感觉受益良多,今天来学习UML状态机图, ...

  9. Android系统自带的层次状态机StateMachine(Hierarchical State Machine)

    Android系统自带的层次状态机StateMachine(Hierarchical State Machine) Android在framework层自己实现一套层次状态机,总共有三个类:State ...

  10. Solana中的跨合约调用 及 Program Derived Addresses

    1. 引言 Solana runtime可通过cross-program invocation机制来支持合约间的相互调用. invoking合约A(Caller) 可触发调用 invoked合约B(C ...

最新文章

  1. 数据中心制冷系统41问答题
  2. 一文了解结构体字节对齐
  3. 一条代码解决各种IE浏览器兼容性问题
  4. Docker 概念解析
  5. tomcat版本_Tomcat9+JDK13环境搭建(新版本)
  6. [Luogu1216][USACO1.5]数字三角形 Number Triangles
  7. 冒泡python代码_用Python写冒泡排序代码
  8. Android音频的播放
  9. Mysql之数据库与sql
  10. VMware使用OVFTool导入虚拟机
  11. 【Julia】 解决安装包下载慢的问题
  12. 【数学建模】灰色模型
  13. Microsoft Remote Desktop 10 - 微软官方免费远程桌面控制 Windows 的软件 APP
  14. 11:c# oop思想面向对象编程(by-朝夕)
  15. C语言实现英寸单位与厘米的转换(两种方法)特简单!!!
  16. RS232及RTS和CTS
  17. 简单的Animation实现角色行走(学习笔记)
  18. Google首席工程师Joshua Bloch谈如何设计优秀的API
  19. multisim安装后无法连接数据库_计算机重装系统时遭遇错误意外重启后无法安装,这是什么原因?...
  20. 《JAVA互联网架构:二期》架构师精品视频课程(免费不加密)

热门文章

  1. 面试常问点:深入剖析JVM的那些事
  2. Android支付实践(一)之支付宝支付详解与demo
  3. oracle的存储过程怎么运行时间,ORACLE 定时运行存储过程经常使用时间间隔
  4. html实现省市县选择,jQuery ajax实现省市县三级联动
  5. 加密算法在windows,linux下的检测办法[md5,sha1]
  6. poythoncode-实战2--常用方式for、while、dict、list
  7. 批处理取系统前一天时间并取备分文件日期为前一天的复制到本地
  8. slf4j打印未捕获异常信息_谁再悄咪咪的吃掉异常,我上去就是一 JIO
  9. flink sink jdbc没有数据_一套 SQL 搞定数据仓库?Flink 有了新尝试
  10. linux 内存坏了,Linux的缓存内存 Cache Memory详解