3

为什么要使用 componentWillReceiveProps / getDerivedStateFromProps ?

在初始化组件数据时, 我们有时需要将组件接收的参数 props 中的数据添加到它的 state 中, 期望组件响应 props 的变化.

然而无论是使用函数声明还是通过 class 声明的组件,都决不能修改自身的 props。所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。这是React 的规则之一。

因此组件接收的 props 数据是只读的, 不可变的, 禁止修改的. 当组件接收了新的 props 时, constructor 函数中数据初始化行为并不会再次发生. 于是我们想要在 componentWillReceiveProps / getDerivedStateFromProps 生命周期函数中获取新的 props 并且调用 setState() 来更新数据.

实际场景

在如下图所示的界面中, 当用户点击了左侧面板中的编辑按钮后, 右侧的表单组件要能够根据用户点击的类目而变化. 同时表单输入域上的任何变化都会实时改变这个组件的数据(这个表单组件内部需要维护 state, 不是一个只接受 props 的完全受控组件) .

image.png

static getDerivedStateFromProps 和 componentWillReceiveProps 的显著区别

尽管 static getDerivedStateFromProps 看起来像是 UNSAFE_componentWillReceiveProps 的替代 API, 但是二者的触发阶段, 参数和可访问的数据都有很大的差异.

getDerivedStateFromProps 并不是 componentWillReceiveProps 的替代品.

触发机制:
  • UNSAFE_componentWillReceiveProps(nextProps) 在组件接收到新的参数时被触发,参数nextProps是变化的Props.

当父组件导致子组件更新的时候, 即使接收的 props 并没有变化, 这个函数也会被调用.

UNSAFE_componentWillReceiveProps()is invoked before a mounted component receives new props.
Note that if a parent component causes your component to re-render, this method will be called even if props have not changed.
  • getDerivedStateFromProps(props, state) 会在每次组件渲染前被调用
getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates.

getDerivedStateFromProps 会在每次组件被重新渲染前被调用, 这意味着无论是父组件的更新, props 的变化, 或是组件内部执行了 setState(), 它都会被调用.

image.png

工作方式
  • UNSAFE_componentWillReceiveProps(nextProps):

参数是组件接收到的新的 props , 用于比对新的 props 和原有的 props, 用户需要在函数体中调用 setState() 来更新组件的数据.

If you need to update the state in response to prop changes (for example, to reset it), you may compare this.props and nextProps and perform state transitions using this.setState() in this method.
// Before
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };
 
  componentDidMount() {
    // this.props.id === undefined
    this._loadData(this.props.id);
  }
 
  componentWillReceiveProps(nextProps) {
      //当某个props中的值发生变化时(此处是id属性)
      if (nextProps.id !== this.props.id) {
        //初始化state中的externalData值为null
        this.setState({externalData: null});
        //基于新的id属性,异步获取数据,并在完成时设置externalData的值
        this._loadData(nextProps.id);
    }
  }
 
  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }
     
  _loadData(id) {
  
    if (!id) {return;}
  
    loadData(id).then(
      externalData => {
        this.setState({externalData});
      }
    );
  }
}
  • static getDerivedStateFromProps(nextProps, currentState):

参数是组件接收到的新的 props 和组件当前的数据. 用户需要在这个函数中返回一个对象, 它将作为 setState() 中的 Updater 更新组件.

It should return an object to update the state, or null to update nothing.
// After Wrong
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
    prevId: this.props.id
  };
 
  static getDerivedStateFromProps(nextProps, prevState) {
    // Store prevId in state so we can compare when props change.
    // Clear out previously-loaded data (so we don't render stale stuff).
    if (nextProps.id !== prevState.prevId) {
      return {
        externalData: null,
        prevId: nextProps.id,
      };
    }
 
    // No state update necessary
    return null;
  }
 
  componentDidUpdate(prevProps, prevState) {
    if (this.state.externalData === null) {
      this._loadData(this.props.id);
    }
  }
 
  componentDidMount() {
    this._loadData(this.props.id);
  }
 
  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }
 
  _loadData(id) {
  
    if (!id) {return;}
  
    loadData(id).then(
      externalData => {
        this.setState({externalData});
      }
    );
  }
}

componentWillReceiveProps标记为deprecated的原因

componentWillReceiveProps标记为deprecated的原因也并不是因为功能问题,而是性能问题。

当外部多个属性在很短的时间间隔之内多次变化,就会导致componentWillReceiveProps被多次调用。这个调用并不会被合并,如果这次内容都会触发异步请求,那么可能会导致多个异步请求阻塞。

getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates. It should return an object to update the state, or null to update nothing.

这个生命周期函数会在每次调用render之前被触发,react setState操作是会通过transaction进行合并的,由此导致的更新过程是batch的,而react中大部分的更新过程的触发源都是setState,所以render触发的频率并不会非常频繁

getDerivedStateFromProps 的缺陷问题

Deriving state leads to verbose code and makes your components difficult to think about.

充满了限制, 很难用

getDerivedStateFromProps 是一个静态方法, 是一个和组件自身"不相关"的角色. 在这个静态方法中, 除了两个默认的位置参数 nextProps 和 currentState 以外, 你无法访问任何组件上的数据.

相比起在 UNSAFE_componentWillReceiveProps(nextProps) 的函数体中直接比较 this.props 和 nextProps 上的字段. 你需要"绕一个弯" 去比较 nextProps 和 currentState.

UNSAFE_componentWillReceiveProps(nextProps){
    if (this.props.currentExercise.id !== nextProps.currentExercise.id){
        this.setState({...nextProps.currentExercise})
    }
}

  
static getDerivedStateFromProps(nextProps, prevState) {

    if (prevState.id !== nextProps.currentExercise.id) {
      return {...nextProps.currentExercise}
    }
    return null;
}
会被频繁地触发

无论是组件调用了 setState(), 接收的 props 发生了变化, 或是父组件的更新都会导致子组件上的 getDerivedStateFromProps被触发.

使用的时候必须非常小心

由于 getDerivedStateFromProps 会在 setState() 后被调用, 并且它的返回值会被用于更新数据. 这意味着你会在 setState() 之后触发 getDerivedStateFromProps, 然后可能意外地再次 "setState()".

getDerivedStateFromProps(nextProps) 函数中的第一个位置参数未必是 "新" 的 props. 在组件内调用了 setState() 时, getDerivedStateFromProps 会被调用. 但是此时的组件其实并没有获得 "新" 的 props, 是的, 这个 nextProps 的值和原来的 props 是一样的.

这就导致了我们在使用 getDerivedStateFromProps 时, 必须添加很多逻辑判断语句来处理 props 上的更新和 state 上的更新, 避免意外地返回了一个 Updater 再次更新数据, 导致数据异常.

查看示例

image.png

更优雅的做法

React 官方博客中提供了以下几种方案:

1.让表单控件变成完全受控组件, 不论是 onChange 处理函数还是 value 都由父组件控制, 这样用户无需再考虑这个组件 props 的变化和 state 的更新.

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

2.让表单控件变成完全--不受控组件, 但是具有 key 属性.(推荐写发)

仍然用自身的数据来控制 value. 但是接收 props 中的某个字段作为 key 属性的值, 以此响应 props 的更新: 当 key 的值变化时 React 会替换 (reset)组件, 从而重新生成初始化数据.
When a key changes, React will create a new component instance rather than update the current one.

示例代码:

//组件内的代码
class EmailInput extends Component {
  state = { email: this.props.defaultEmail };

  handleChange = event => {
    this.setState({ email: event.target.value });
  };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }
}
// 在父组件中接收 props 中的数据作为 key
<EmailInput
  defaultEmail={this.props.user.email}
  key={this.props.user.id}
/>

3.其他方法请参考 这里

如何使用getDerivedStateFromProps进行异步的处理呢?

If you need to perform a side effect (for example, data fetching or an animation) in response to a change in props, use componentDidUpdate lifecycle instead.

官方教你怎么写代码系列,但是其实也没有其他可以进行异步操作的地方了。为了响应props的变化,就需要在componentDidUpdate中根据新的props和state来进行异步操作,比如从服务端拉取数据。

// 在getDerivedStateFromProps中进行state的改变
static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.type !== prevState.type) {
        return {
            type: nextProps.type,
        };
    }
    return null;
}
// 在componentDidUpdate中进行异步操作,驱动数据的变化
componentDidUpdate() {
    this._loadAsyncData({...this.state});
}
派生状态

派生状态使用getDerivedStateFromProps

【参考文献】简书
React 官网-博客
React博客


一吃三大碗
130 声望9 粉丝

“唯有深入,方能浅出”