getDerivedStateFromProps及派生state

2,881 阅读5分钟

getDerivedStateFromProps有什么用?

React生命周期的命名一直都是非常语义化的,这个生命周期的意思就是从props中获取派生state,意思非常明确。
可以说,这个生命周期的功能实际上就是将传入的props映射到state上面,基本上就是直接替换了原来的componentWillReceiveProps。

如何使用?

getDerivedStateFromProps接受两个参数props和state,根据变化来返回最新的state,如果不变化就返回null。

export default class Parent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            number: 0,
        };
    }

    handleClick = () => {
        this.setState({
            number: this.state.number + 1,
        });
    };

    render() {
        const { number } = this.state;
        return (
            <div>
                <Child number={number} />
                <button onClick={this.handleClick}>add one</button>
            </div>
        );
    }
}

class Child extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            number: props.number,
        };
    }

    static getDerivedStateFromProps(props, state) {
        if (props.number !== state.number) {
            return { number: props.number };
        }
        // 没有变化就返回null 
        return null;
    }

    render() {
        const { number } = this.state;
        return <div>number is: {number}</div>;
    }
}

和componentWillReceiveProps有什么区别?

但是在16.4版本之后,事情变得有意思了(16.4之前的版本setState并不会进入getDerivedStateFromProps),这个函数会在每次re-rendering之前被调用,这意味着什么呢?
意味着即使你的props没有任何变化,而是本身的state发生了变化,导致子组件发生了re-render,这个生命周期函数依然会被调用。看似一个非常小的修改,却可能会导致很多隐含的问题。

export default class Demo1 extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            number: 0,
        };
    }

    static getDerivedStateFromProps(props, state) {
        console.log('getDerivedStateFromProps');
    }

    handleClick = () => {
        this.setState({
            number: this.state.number + 1,
        });
    };

    render() {
        console.log('render');
        const { number } = this.state;
        return (
            <div>
                <div>{number}</div>
                <button onClick={this.handleClick}>add one</button>
            </div>
        );
    }
}

这个例子中一开始就会打印一次getDerivedStateFromProps,每点击一次button,都会先打印getDerivedStateFromProps,然后打印render。说明getDerivedStateFromProps这个生命周期已经和我们对componentWillReceiveProps的理解不一样了,只要有重新渲染都会进来(包括第一次渲染的时候)。

坑从这里开始。

采坑:setState不会引发重新渲染

由于setState也会进入getDerivedStateFromProps,所以上面的代码会导致setState时,组件不会重新渲染。

export default class Parent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            number: 0,
        };
    }

    handleClick = () => {
        this.setState({
            number: this.state.number + 1,
        });
    };

    render() {
        const { number } = this.state;
        return (
            <div>
                <Child number={number} />
                <button onClick={this.handleClick}>add one(outer)</button>
            </div>
        );
    }
}

class Child extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            number: props.number,
        };
    }

    static getDerivedStateFromProps(props, state) {
        if (props.number !== state.number) {
            // 即使是setState引发的变化这里return的也是props,所以setState不会引发组件重新渲染
            return { number: props.number };
        }
        return null;
    }

    handleClick = () => {
        this.setState({
            number: this.state.number + 1,
        });
    };

    render() {
        const { number } = this.state;
        return (
            <div>
                <div>number is: {number}</div>
                {/* 这个按钮点击无效 */}
                <button onClick={this.handleClick}>add one(inner)</button>
            </div>
        );
    }
}

填坑:只有当props变化时,才重新返回state

// getDerivedStateFromProps和componentWillReceiveProps的区别
// getDerivedStateFromProps与setState并用出现的问题
export default class Parent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            number: 0,
        };
    }

    handleClick = () => {
        this.setState({
            number: this.state.number + 1,
        });
    };

    render() {
        const { number } = this.state;
        return (
            <div>
                <Child number={number} />
                <button onClick={this.handleClick}>add one(outer)</button>
            </div>
        );
    }
}

class Child extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            number: props.number,
        };
    }

    static getDerivedStateFromProps(props, state) {
       // 只有props引发的变化才会进入这里
        if (props.number !== state.prevNumber) {
            return {
                number: props.number,
                prevNumber: props.number,
            };
        }
        console.log('change from setState');
        return null;
    }

    handleClick = () => {
        this.setState({
            number: this.state.number + 1,
        });
    };

    render() {
        const { number } = this.state;
        return (
            <div>
                <div>number is: {number}</div>
                <button onClick={this.handleClick}>add one(inner)</button>
            </div>
        );
    }
}

通过保存一个之前 的prop 值,我们就可以在只有 prop 变化时才去修改 state。这样就解决上述的问题。
出现的另外一个问题是,当我们通过Child组件的setState改变number之后,再通过父组件的props改变,number会被重置,这问题其实就是由于数据源的多样性所导致的,父组件和子组件都在控制这个状态,而两边的状态是不一致的。

注意:这里有一个比较容易令人迷惑的点,当prop引发的变化进入该生命周期时,如果return null,表示不进行重新渲染,当state引发的变化进入该生命周期时,如果return null,表示按照既有state重新渲染。其实归根到底就是将return的state与之前的state进行合并后,再交由componentShouldUpdate进行对比,决定是否要重新渲染。

到底该不该用getDerivedStateFromProps

大多数情况下,我们是不应该使用getDerivedStateFromProps的,我们总能找到更加更加合适且可靠的方式去维护状态。

场景一:prop和state同时控制一个状态改变

正如上述案例描述的那样,当数据源同时来自prop和state时,会产生数据混乱的问题,此时,我们应当尽可能将数据交由父组件管理,完全由prop来改变状态,这就是所谓的完全受控组件

<Child number={number} onChange={this.handleClick} />

场景二:prop未发生变化,但是state需要重置

除了上述,数据源多样性所导致的问题之外,还有一个问题,组件只会在 prop 改变时才会改变。想象一下,如果有一个密码输入组件,拥有同样 email 的两个账户进行切换时,这个输入框不会重置(用来让用户重新登录)。因为父组件传来的 prop 值没有变化!这会让用户非常惊讶,因为这看起来像是帮助一个用户分享了另外一个用户的密码,(查看这个示例)。
我们可以使用 key 这个特殊的 React 属性。当 key 变化时, React 会创建一个新的而不是更新一个既有的组件。 Keys 一般用来渲染动态列表,但是这里也可以使用。当用户输入时,我们使用 user ID 当作 key 重新创建一个新的 email input 组件:

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

每次 ID 更改,都会重新创建 EmailInput ,并将其状态重置为最新的 defaultEmail 值。(点击查看这个模式的演示) 使用此方法,不用为每次输入都添加 key,在整个表单上添加 key 更有位合理。每次 key 变化,表单里的所有组件都会用新的初始值重新创建。

大部分情况下,这是处理重置 state 的最好的办法。

总结

在实际应用中,如果每个值都有明确的来源,就可以避免上面提到的反面模式,用单一源来控制会使得数据可靠得多。getDerivedStateFromProps是一个高级复杂的功能,应该保守使用。