一.对componentWillReceiveProps的误解 componentWillReceiveProps通常被认为是propsWillChange,我们确实也通过它来判断props change。但实际上,componentWillReceiveProps在每次rerender时都会调用,无论props变了没:


class A extends React.Component {
  render() {
    return <div>Hello {this.props.name}</div>;
  }

  componentWillReceiveProps(nextProps) {
    console.log('Running A.componentWillReceiveProps()');
  }
}

class B extends React.Component {
  constructor() {
    super();
    this.state = { counter: 0 };
  }

  render() {
    return <A name="World" />
  }

  componentDidMount() {
    setInterval(() => {
      this.setState({
        counter: this.state.counter + 1
      });
    }, 1000)
  }
}

ReactDOM.render(<B/>, document.getElementById('container'));

上例中,父组件B的state change引发子组件A的render及componentWillReceiveProps被调用了,但A并没有发生props change

没错,只要接到了新的props,componentWillReceiveProps就会被调用,即便新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.

相关实现如下:


updateComponent: function () {
  var willReceive = false;
  var nextContext;

  if (this._context !== nextUnmaskedContext) {
    nextContext = this._processContext(nextUnmaskedContext);
    willReceive = true;
  }

  // Not a simple state update but a props update
  if (prevParentElement !== nextParentElement) {
    willReceive = true;
  }

  if (willReceive && inst.componentWillReceiveProps) {
    inst.componentWillReceiveProps(nextProps, nextContext);
  }
}

(摘自典藏版ReactDOM v15.6.1)

也就是说,componentWillReceiveProps的调用时机是:

引发当前组件更新 && (context发生变化 || 父组件render结果发生变化,即当前组件需要rerender) 注意,这里并没有对props做diff:


React doesn’t make an attempt to diff props for user-defined components so it doesn’t know whether you’ve changed them.

因为props值没什么约束,难以diff:

Oftentimes a prop is a complex object or function that’s hard or impossible to diff, so we call it always (and rerender always) when a parent component rerenders.

唯一能保证的是props change一定会触发componentWillReceiveProps,但反之不然:


The only guarantee is that it will be called if props change.

P.S.更多相关讨论见Documentation for componentWillReceiveProps() is confusing

二.如何理解getDerivedStateFromProps getDerivedStateFromProps是用来替代componentWillReceiveProps的,应对state需要关联props变化的场景:


getDerivedStateFromProps exists for only one purpose. It enables a component to update its internal state as the result of changes in props.

即允许props变化引发state变化(称之为derived state,即派生state),虽然多数时候并不需要把props值往state里塞,但在一些场景下是不可避免的,比如:

记录当前滚动方向(recording the current scroll direction based on a changing offset prop)

取props发请求(loading external data specified by a source prop)

这些场景的特点是与props变化有关,需要取新旧props进行比较/计算,

与componentWillReceiveProps类似,getDerivedStateFromProps也不只是在props change时才触发,具体而言,其触发时机为:


With React 16.4.0 the expected behavior is for getDerivedStateFromProps to fire in all cases before shouldComponentUpdate.

更新流程中,在shouldComponentUpdate之前调用。也就是说,只要走进更新流程(无论更新原因是props change还是state change),就会触发getDerivedStateFromProps

就具体实现而言,与计算nextContext(nextContext = this._processContext(nextUnmaskedContext))类似,在确定是否需要更新(shouldComponentUpdate)之前,要先计算nextState:


export function applyDerivedStateFromProps(
  workInProgress: Fiber,
  ctor: any,
  getDerivedStateFromProps: (props: any, state: any) => any,
  nextProps: any,
) {
  const prevState = workInProgress.memoizedState;

  const partialState = getDerivedStateFromProps(nextProps, prevState);

  // Merge the partial state and the previous state.
  const memoizedState =
    partialState === null || partialState === undefined
      ? prevState
      : Object.assign({}, prevState, partialState);
  workInProgress.memoizedState = memoizedState;

  // Once the update queue is empty, persist the derived state onto the
  // base state.
  const updateQueue = workInProgress.updateQueue;
  if (updateQueue !== null && workInProgress.expirationTime === NoWork) {
    updateQueue.baseState = memoizedState;
  }
}

(摘自react/packages/react-reconciler/src/ReactFiberClassComponent.js)

getDerivedStateFromProps成了计算nextState的必要环节:


getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates.

function mountIndeterminateComponent(
  current,
  workInProgress,
  Component,
  renderExpirationTime,
) {
  workInProgress.tag = ClassComponent;
  workInProgress.memoizedState =
    value.state !== null && value.state !== undefined ? value.state : null;

  const getDerivedStateFromProps = Component.getDerivedStateFromProps;
  if (typeof getDerivedStateFromProps === 'function') {
    applyDerivedStateFromProps(
      workInProgress,
      Component,
      getDerivedStateFromProps,
      props,
    );
  }

  adoptClassInstance(workInProgress, value);
  mountClassInstance(workInProgress, Component, props, renderExpirationTime);
  // 调用render,第一阶段结束
  return finishClassComponent(
    current,
    workInProgress,
    Component,
    true,
    hasContext,
    renderExpirationTime,
  );
}

(摘自react/packages/react-reconciler/src/ReactFiberBeginWork.js)

所以在首次渲染时也会调用,这是与componentWillReceiveProps相比最大的区别

三.派生state实践原则 实现派生state有两种方式:

getDerivedStateFromProps:从props派生出部分state,其返回值会被merge到当前state

componentWillReceiveProps:在该生命周期函数里setState

实际应用中,在两种常见场景中容易出问题(被称为anti-pattern,即反模式):

props变化时无条件更新state

更新state中缓存的props

在componentWillReceiveProps时无条件更新state,会导致通过setState()手动更新的state被覆盖掉,从而出现非预期的状态丢失:


When the source prop changes, the loading state should always be overridden. Conversely, the state is overridden only when the prop changes and is otherwise managed by the component.

例如(仅以componentWillReceiveProps为例,getDerivedStateFromProps同理):


class EmailInput extends Component {
  state = { email: this.props.email };

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

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

  componentWillReceiveProps(nextProps) {
    // This will erase any local state updates!
    // Do not do this.
    this.setState({ email: nextProps.email });
  }
}

上例中,用户在input控件中输入一串字符(相当于手动更新state),如果此时父组件更新引发该组件rerender了,用户输入的内容就被nextProps.email覆盖掉了(见在线Demo),出现状态丢失

针对这个问题,我们一般会这样解决:


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
      });
    }
  }
}

精确限定props change到email,不再无条件重置state。似乎完美了,真的吗?

其实还存在一个尴尬的问题,有些时候需要从外部重置state(比如重置密码输入),而限定state重置条件之后,来自父组件的props.email更新不再无条件传递到input控件。所以,之前可以利用引发EmailInput组件rerender把输入内容重置为props.email,现在就不灵了

那么,需要想办法从外部把输入内容重置回props.email,有很多种方式:

EmailInput提供resetValue()方法,外部通过ref调用

外部改变EmailInput的key,强制重新创建一个EmailInput,从而达到重置回初始状态的目的

嫌key杀伤力太大(删除重建,以及组件初始化成本),或者不方便(key已经有别的作用了)的话,添个props.myKey结合componentWillReceiveProps实现局部状态重置

其中,第一种方法只适用于class形式的组件,后两种则没有这个限制,可根据具体场景灵活选择。第三种方法略绕,具体操作见Alternative 1: Reset uncontrolled component with an ID prop

类似的场景之所以容易出问题,根源在于:


when a derived state value is also updated by setState calls, there isn’t a single source of truth for the data.

一边通过props计算state,一边手动setState更新,此时该state有两个来源,违背了组件数据的单一源原则

解决这个问题的关键是保证单一数据源,杜绝不必要的拷贝:


For any piece of data, you need to pick a single component that owns it as the source of truth, and avoid duplicating it in other components.

所以有两种方案(砍掉一个数据源即可):

完全去掉state,这样就不存在state与props的冲突了

忽略props change,仅保留第一次传入的props作为默认值

两种方式都保证了单一数据源(前者是props,后者是state),这样的组件也可以称之为完全受控组件与完全不受控组件

四.“受控”与“不受控” 组件分为受控组件与不受控组件,同样,数据也可以这样理解

受控组件与不受控组件 针对表单输入控件(<input>、<textarea>、<select>等)提出的概念,语义上的区别在于受控组件的表单数据由React组件来处理(受React组件控制),而不受控组件的表单数据交由DOM机制来处理(不受React组件控制)

受控组件维护一份自己的状态,并根据用户输入更新这份状态:


An input form element whose value is controlled by React is called a controlled component. When a user enters data into a controlled component a change event handler is triggered and your code decides whether the input is valid (by re-rendering with the updated value). If you do not re-render then the form element will remain unchanged.

用户与受控组件交互时,用户输入反馈到UI与否,取决于change事件对应的处理函数(是否需要改变内部状态,通过rerender反馈到UI),用户输入受React组件控制,例如:


class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(event) {
    // 在这里决定是否把输入反馈到UI
    this.setState({value: event.target.value});
  }

  render() {
    return (
      <input type="text" value={this.state.value} onChange={this.handleChange} />
    );
  }
}

不受控组件不维护这样的状态,用户输入不受React组件控制:


An uncontrolled component works like form elements do outside of React. When a user inputs data into a form field (an input box, dropdown, etc) the updated information is reflected without React needing to do anything. However, this also means that you can’t force the field to have a certain value.

用户与不受控组件的交互不受React组件控制,输入会立即反馈到UI。例如:


class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.input = React.createRef();
  }

  handleSubmit(event) {
    // input的输入直接反馈到UI,仅在需要时从DOM读取
    alert('A name was submitted: ' + this.input.current.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={this.input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

从数据角度看受控与不受控 不受控组件把DOM当作数据源:


An uncontrolled component keeps the source of truth in the DOM.

而受控组件把自身维护的state当作数据源:


Since the value attribute is set on our form element, the displayed value will always be this.state.value, making the React state the source of truth.

让程序行为可预测的关键在于减少变因,即保证唯一数据源。那么就有数据源唯一的组件,称之为完全受控组件与完全不受控组件

对应到之前派生state的场景,就有了这两种解决方案:


// 完全不受控组件,不再维护输入state
function EmailInput(props) {
  return <input onChange={props.onChange} value={props.email} />;
}

// 完全受控组件,只维护自己的state,不接受来自props的更新
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到state的场景,要么考虑把props收进来完全作为自己的state,不再受外界影响(使数据受控):


Instead of trying to “mirror” a prop value in state, make the component controlled

要么把自己的state丢掉,完全放弃对数据的控制:


Remove state from our component entirely.

五.缓存计算结果 另一些时候,拷贝props到state是为了缓存计算结果,避免重复计算

例如,常见的列表项按输入关键词筛选的场景:


class Example extends Component {
  state = {
    filterText: "",
  };

  static getDerivedStateFromProps(props, state) {
    if (
      props.list !== state.prevPropsList ||
      state.prevFilterText !== state.filterText
    ) {
      return {
        prevPropsList: props.list,
        prevFilterText: state.filterText,
        // 缓存props结算结果到state
        filteredList: props.list.filter(item => item.text.includes(state.filterText))
      };
    }
    return null;
  }

  handleChange = event => {
    this.setState({ filterText: event.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>
    );
  }
}

能用,但过于复杂了。通过getDerivedStateFromProps创造了另一个变因(state.filteredList),这样props change和state change都可能影响筛选结果,容易出问题

事实上,想要避免重复计算的话,并不用缓存一份结果到state,比如:


class Example extends PureComponent {
  state = {
    filterText: ""
  };

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

  render() {
    const filteredList = this.props.list.filter(
      item => item.text.includes(this.state.filterText)
    )

    return (
      <Fragment>
        <input onChange={this.handleChange} value={this.state.filterText} />
        <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
      </Fragment>
    );
  }
}

利用PureComponent的render()只在props change或state change时才会再次调用的特性,直接在render()里放心做计算

看起来很完美,但实际场景的state和props一般不会这么单一,如果另一个计算无关的props或state更新了也会引发rerender,产生重复计算

所以干脆抛开“不可靠”的PureComponent,这样解决:


import memoize from "memoize-one";

class Example extends Component {
  state = { filterText: "" };

  filter = memoize(
    (list, filterText) => list.filter(item => item.text.includes(filterText))
  );

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

  render() {
    const filteredList = this.filter(this.props.list, this.state.filterText);

    return (
      <Fragment>
        <input onChange={this.handleChange} value={this.state.filterText} />
        <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
      </Fragment>
    );
  }
}

不把计算结果放到state里,也不避免rerender,而是缓存到外部,既干净又可靠

参考资料 You Probably Don’t Need Derived State

React.Component

When “getDerivedStateFromProps” is invoked on Update phase?

Controlled vs. Uncontrolled Components