zoukankan      html  css  js  c++  java
  • 【React】417- React中componentWillReceiveProps的替代升级方案

    作者 | 曹清达

    因为最近在做一个逻辑较为复杂的需求,在封装组件时经常遇到父组件props更新来触发子组件的state这种情景。在使用componentWillReceiveProps时,发现React官网已经把componentWillReceiveProps重名为UNSAFE_componentWillReceiveProps,但是我发现了getDerivedStateFromProps可以替代,却又被一篇博客告知这个也尽量别使用。因为组件一旦使用派生状态,很有可能因为没有明确的数据来源导致出现一些bug和不一致性。既然提倡避免使用,肯定也会有相应的解决方案。

    本文会介绍以上两种生命周期的使用方法、误区和替代升级方案。

    componentWillReceiveProps

    1.介绍

    componentWillReceiveProps是React生命周期函数之一,在初始props不会被调用,它会在组件接受到新的props时调用。一般用于父组件更新状态时子组件的重新渲染。在react16.3之前,componentWillReceiveProps是在不进行额外render的前提下,响应props中的改变并更新state的唯一方式。

    2.使用方法

    1. componentWillReceiveProps(nextProps) {

    2. //通过this.props来获取旧的外部状态,初始 props 不会被调用

    3. //通过对比新旧状态,来判断是否执行如this.setState及其他方法

    4. }

    主要在以下两种情景使用:

    1. 从上传的props无条件的更新state

    2. 当props和state不匹配时候更新state

    3.常见误区

    无条件的更新state

    1. class EmailInput extends Component {

    2. state = { email: this.props.email };

    3. componentWillReceiveProps(nextProps) {

    4. // 这样会覆盖内部 email的更新!

    5. this.setState({ email: nextProps.email });

    6. }

    7. handleChange = event => {

    8. this.setState({ email: event.target.value });

    9. };

    10. render() {

    11. return <input onChange={this.handleChange} value={this.state.email} />;

    12. }

    13. }

    从上述例子可以发现子组件的更新会被父组件的更新覆盖。并且大家在使用过程没有必要这样无条件更新,完全可以写成一个完全受控组件。

    1. <input onChange={this.props.onChange} value={this.props.email} />

    也可以对比新旧props状态:

    1. componentWillReceiveProps(nextProps) {

    2. if (nextProps.email !== this.props.email) {

    3. this.setState({ email: nextProps.email });

    4. }

    5. }

    现在该组件只会在props改变时候覆盖之前的修改了,但是仍然存在一个小问题。例如一个密码管理网站使用了如上的输入组件。当切换两个不同的账号的时候,如果这两个账号的邮箱相同,那么我们的重置就会失效。因为对于这两个账户传入的email属性是一样的,即数据源相同。效果如下: 

    1. 父组件:

    2. import React, { Component, Fragment } from 'react';

    3. import { Radio } from 'antd';

    4. import UncontrolledInput from './UncontrolledInput';

    5. const accounts = [

    6. {

    7. id: 1,

    8. name: 'One',

    9. email: 'same.email@example.com',

    10. },

    11. {

    12. id: 2,

    13. name: 'Two',

    14. email: 'same.email@example.com',

    15. },

    16. {

    17. id: 3,

    18. name: 'Three',

    19. email: 'other.fake.email@example.com',

    20. }

    21. ];

    22. export default class AccountList extends Component {

    23. state = {

    24. selectedIndex: 0

    25. };

    26. render() {

    27. const { selectedIndex } = this.state;

    28. return (

    29. <Fragment>

    30. <UncontrolledInput email={accounts[selectedIndex].email} />

    31. <Radio.Group onChange={(e) => this.setState({ selectedIndex: e.target.value })} value={selectedIndex}>

    32. {accounts.map((account, index) => (

    33. <Radio value={index}>

    34. {account.name}

    35. </Radio>

    36. ))}

    37. </Radio.Group>

    38. </Fragment>

    39. );

    40. }

    41. }

    42. //子组件

    43. import React, { Component } from 'react';

    44. import { Input } from 'antd';

    45. export default class UncontrolledInput extends Component {

    46. state = {

    47. email: this.props.email

    48. };

    49. componentWillReceiveProps(nextProps) {

    50. if (nextProps.email !== this.props.email) {

    51. this.setState({ email: nextProps.email });

    52. }

    53. }

    54. handleChange = event => {

    55. this.setState({ email: event.target.value });

    56. };

    57. render() {

    58. return (

    59. <div>

    60. Email: <Input onChange={this.handleChange} value={this.state.email} />

    61. </div>

    62. );

    63. }

    64. }

    从id为1的账户切换到id为2的账户,因为传入的email相同(nextProps.email === this.props.email),输入框无法重置。从id为2的账户切换到id为3的账户,因为传入的email不同,进行了输入框的重置。大家可能想到,既然需要切换账户就重置,那就把id或者selectedIndex选中项作为判断重置条件。

    1. //父组件引入子组件方式

    2. <UncontrolledInput email={accounts[selectedIndex].email} selectedIndex={selectedIndex} />

    3. //子组件

    4. componentWillReceiveProps(nextProps) {

    5. if (nextProps.selectedIndex !== this.props.selectedIndex) {

    6. this.setState({ email: nextProps.email });

    7. }

    8. }

    其实当使用唯一标识符来判来保证子组件有一个明确的数据来源时,我们使用key是获取是最合适的方法。并且不需要使用componentWillReceiveProps,只需要保证每次我们每次需要重置输入框时候可以有不同的key值。

    1. //父组件

    2. <UncontrolledInput email={accounts[selectedIndex].email} key={selectedIndex} />

    每当key发生变化,会引起子组件的重新构建而不是更新。当我们切换账户,不再是子组件而是重新构建,同样的达到了重置的效果。但是还有一个小问题,当我们在一个账户做了更改之后,切换到其他账户并切换回来,发现我们的之前的更改不会缓存。这里我们可以将输入框设计为一个完全可控组件,将更改的状态存在父组件中。

    1. //父组件

    2. export default class Release extends Component {

    3. state = {

    4. selectedIndex: 0,

    5. valueList: [...accounts]

    6. };

    7. onChange = (event) => {

    8. const { valueList, selectedIndex } = this.state;

    9. valueList[selectedIndex].email = event.target.value;

    10. this.setState({ valueList: [...valueList] });

    11. }

    12. render() {

    13. const { selectedIndex, valueList } = this.state;

    14. return (

    15. <Fragment>

    16. <UncontrolledInput email={valueList[selectedIndex].email} onChange={this.onChange} />

    17. <Radio.Group onChange={(e) => this.setState({ selectedIndex: e.target.value })} value={selectedIndex}>

    18. {valueList.map((account, index) => (

    19. <Radio value={index}>

    20. {account.name}

    21. </Radio>

    22. ))}

    23. </Radio.Group>

    24. </Fragment>

    25. );

    26. }

    27. }

    28. //子组件

    29. export default class UncontrolledInput extends Component {

    30. state = {

    31. email: this.props.email

    32. };

    33. render() {

    34. return (

    35. <div>

    36. Email: <Input onChange={this.props.onChange} value={this.props.email} />

    37. </div>

    38. );

    39. }

    40. }

    替换方案:getDerivedStateFromProps

    1.介绍

    React在版本16.3之后,引入了新的生命周期函数getDerivedStateFromProps 需要注意的一点,在React 16.4^ 版本中getDerivedStateFromProps 比 16.3 版本中多了setState forceUpdate 两种触发方法。 

    详情请看官方给出的生命周期图。

    2.使用

    1. static getDerivedStateFromProps(nextProps,prevState){

    2. //该方法内禁止访问this

    3. if(nextProps.email !== prevState.email){

    4. //通过对比nextProps和prevState,返回一个用于更新状态的对象

    5. return {

    6. value:nextProps.email

    7. }

    8. }

    9. //不需要更新状态,返回null

    10. return null

    11. }

    如果大家仍需要通过this.props来做一些事,可以使用componentDidUpdate

    1. componentDidUpdate(prevProps, prevState, snapshot){

    2. if(this.props.email){

    3. // 做一些需要this.props的事

    4. }

    5. }

    通过以上使用方法,React相当于把componentWillReceiveProps拆分成getDerivedStateFromProps和componentDidUpdate。拆分后,使得派生状态更加容易预测。

    3.常见误区

    当我们在子组件内使用该方法来判断新props和state时,可能会引起内部更新无效。

    1. //子组件

    2. export default class UncontrolledInput extends Component {

    3. state = {

    4. email: this.props.email

    5. };

    6. static getDerivedStateFromProps(props, state) {

    7. if (props.email !== state.email) {

    8. return {

    9. email: props.email

    10. };

    11. }

    12. return null;

    13. }

    14. handleChange = event => {

    15. this.setState({ email: this.props.email });

    16. };

    17. render() {

    18. return (

    19. <div>

    20. Email: <Input onChange={this.handleChange} value={this.state.email} style={style} />

    21. </div>

    22. );

    23. }

    24. }

    这是因为在 React 16.4^ 的版本中,setState 和 forceUpdate 也会触发getDerivedStateFromProps方法。当我们尝试改变输入框值,触发setState方法,进而触发该方法,并把 state 值更新为传入的 props。虽然在getDerivedStateFromProps中,不能访问this.props,但是我们可以新加个字段来间接访问this.props进而判断新旧props。

    1. export default class UncontrolledInput extends Component {

    2. state = {

    3. email: this.props.email,

    4. prevPropEmail: ''

    5. };

    6. static getDerivedStateFromProps(props, state) {

    7. if (props.email !== state.prevPropEmail) {

    8. return {

    9. email: props.email,

    10. prevPropEmail: props.email,

    11. };

    12. }

    13. return null;

    14. }

    15. handleChange = event => {

    16. this.setState({ email: this.props.email });

    17. };

    18. render() {

    19. return (

    20. <div>

    21. Email: <Input onChange={this.handleChange} value={this.state.email} style={style} />

    22. </div>

    23. );

    24. }

    25. }

    通过上一个props.email来判断是否更新,而不是通过state的状态。虽然解决了内部更新问题,但是并不能解决componentWillReceiveProps中提到的多个账户切换无法重置等问题。并且这样写的派生状态代码冗余,并使组件难以维护。

    升级方案

    我们在开发过程中很难保证每个数据都有明确的数据来源,尽量避免使用这两个生命周期函数。结合以上例子以及官网提供的方法,我们有以下升级方案: 1.完全受控组件(推荐) 2.key标识的完全不可控组件(推荐) 使用React的key属性。通过传入不同的key来重新构建组件。在极少情况,你可能需要在没有合适的ID作为key的情况下重置state,可以将需要重置的组件的key重新赋值为当前时间戳。虽然重新创建组件听上去会很慢,但其实对性能的影响微乎其微。并且如果组件具有很多更新上的逻辑,使用key甚至可以更快,因为该子树的diff得以被绕过。 3.通过唯一属性值重置非受控组件。 因为使用key值我们会重置子组件所有状态,当我们需要仅重置某些字段时或者子组件初始化代价很大时,可以通过判断唯一属性是否更改来保证重置组件内部状态的灵活性。 4.使用实例方法重置非受控组件。 当我们没有合适的特殊属性去匹配的时候,可以通过实例方法强制重置内部状态

    1. //父组件

    2. handleChange = index => {

    3. this.setState({ selectedIndex: index }, () => {

    4. const selectedAccount = accounts[index];

    5. this.inputRef.current.resetEmailForNewUser(selectedAccount.email);

    6. });

    7. };

    8. //子组件

    9. resetEmailForNewUser(defaultEmail) {

    10. this.setState({ email: defaultEmail });

    11. }

    总结

    升级方案不仅仅以上几种,例如当我们仅仅需要当props更改进行数据提取或者动画时,可以使用componentDidUpdate。还可以参考官网提供的memoization(缓存记忆)。但是主要推荐的方案是完全受控组件和key值的完全不受控组件。当无法满足需求的特殊情况,再使用其他方法。总之,componentWillReceiveProps/getDerivedStateFromProps是一个拥有一定复杂度的高级特性,我们应该谨慎地使用。

    原创系列推荐
    1. JavaScript 重温系列(22篇全)
    2. ECMAScript 重温系列(10篇全)
    3. JavaScript设计模式 重温系列(9篇全)
    4. 正则 / 框架 / 算法等 重温系列(16篇全)
    5. Webpack4 入门(上)|| Webpack4 入门(下)
    6. MobX 入门(上) ||  MobX 入门(下)
    7. 59篇原创系列汇总

    回复“加群”与大佬们一起交流学习~

    点这,与大家一起分享本文吧~
    个人博客:http://www.pingan8787.com 微信公众号【前端自习课】和千万网友一起,每日清晨,享受一篇前端优秀文章。 目前已连续推送文章 600+ 天,愿每个人的初心都能一直坚持下去!
  • 相关阅读:
    备用
    Python进阶
    *args 和 **kwargs
    C语言
    【Pythno库】-Re
    【C语言】-struct
    Django By Example
    字符串
    Python库
    【Keil】Keil5-改变字的大小和颜色
  • 原文地址:https://www.cnblogs.com/pingan8787/p/13069602.html
Copyright © 2011-2022 走看看