yanni4night

如何管理 React 中的 state

yanni4night · 2016-08-22翻译 · 2589阅读 原文链接

动机

最近经常有人争论该如何管理 React 中的状态(state)。一些声称 setState() 并没有按照预想的那样工作,他们于是完全抛弃了使用组件自身的状态,而不得不使用一个类似 Flux 的状态容器。

从另一方面讲,有人担心“这些误解有可能导致教条的行为”。

仅仅因为有趣或者一些教程告诉你这样做就引入了一个外部的状态容器,听上去并不是一个靠谱的技术决策标准。

我认为,ReduxMobX 以及其它你能想到的状态容器都没有错。实际上它们都相当有用,并催生出了能满足你所有需求的生态系统。问题是:你可能一点也不需它们,或者不全需要

如果你刚刚入门 React,Pete Hunt 为你推荐l React 史上最佳建议。即使是 Redux 和 React 的核心开发者成员 Dan Abramov 也推荐它。

也就是说,在了解 Flux 之前你需要知道如何处理 React 中的状态(state)。

假设你正准备启动开发一个真正实用的应用,你都可以不使用 Flux 就能完成大部分的工作。我们把组件分为两类:容器和展现,这样你可以保证可维护性和复用性。同样,考虑到未来可能引入 Flux,迁移路线上的障碍也就会被扫清了。给你一个在最后回复时刻上做出决定的选择。

即使你已经在使用 Flux 了,那么也可以有使用组件状态(component state)的场景。想想跨项目的组件库,其组件都是独立的。你肯定不会想把状态容器当做依赖的。

我的观点是:

  • 业界对如何管理 React 的 state 有相左意见、误解以及无知。
  • 为了迎合 React 提供的所有能力,对于如何处理状态具有一致的理解是很重要的。
  • 如果不需要的话,不要再引入多余的复杂层次。记住简单最重要

下面的 FAQ 部分是为了澄清 React 状态管理中的错综复杂的疑虑。


FAQ

state 是如何工作的?

React 组件就像一个能展现用户界面的状态机一样。用户的每个动作都可能触发状态机的变化,新的状态由不同的 React 元素展现。

React 在 this.state 中存储组件的状态。有两种设置 this.state 初始值的方法。每种都与你创建组件的方式有关:

// 使用 React.createClass
var Counter = React.createClass({
    getInitialState: function() {
        return {counter: 0};
    },
    ...
});

// 使用 ES6 类
class Counter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {counter: 0};
    }
    ...
}

改变组件状态的方式:

this.setState(data, callback);

这个方法暗地里把 data 合并到 this.state,并且重新渲染了组件。data 参数可以是对象也可以是返回包含要更新字段的对象的函数。可选的 callback 会在组件重渲染后被调用。你应该很少会用到这个回调,因为 React 已经帮你更新了用户界面。

让我们看看例子:

class Counter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {count: 0};
        this.incrementCounter = this.updateCounter.bind(this, 1);
        this.decrementCounter = this.updateCounter.bind(this, -1);
    }

    render() {
        return (
            <div>
                <div>{this.state.count}</div>
                <input type='button' value='+' onClick={this.incrementCounter} />
                <input type='button' value='-' onClick={this.decrementCounter} />
            </div>
        );
    }

    updateCounter(count) {
        this.setState({count: this.state.count + count});
    }
}

state 中应该保存什么?

Dan Abramov 在推特上回答了这个问题。基本意思是不要保存 props 的计算值,也不要保存 render() 不使用的状态。举例说明:

// 不要在 state 重复 props 的数据
// 反例
class Component extends React.Component {
    constructor(props) {
        super(props);
        this.state = {message: props.message};
    }

    render() {
        return <div>{this.state.message}</div>;
    }
}

上面例子中的问题是 state 只有在第一次创建组件时才赋值。当 props 变化后,state 不会变,因此界面就不会变。于是你不得不在 componentWillReceiveProps() 中更新 state,导致真实数据源的重复。

// 更优的例子

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

在 state 中存储 props 的计算值中会导致同样的问题:

// 不要保持 props 计算值的句柄
// 反例

class Component extends React.Component {
    constructor(props) {
        super(props);
        this.state = {fullName: `${props.name} ${props.lastName}`};
    }

    render() {
        return <div>{this.state.fullName}</div>;
    }
}

// 更优的实践

class Component extends React.Component {
    render() {
        const {name, lastName} = this.props;
        return <div>{`${name} ${lastName}`}</div>;
    }
}

当然,如果明确只是初始值的话,那么基于 props 设置初始的 state 倒没什么关系。

// 非反例

class Component extends React.Component {
    constructor(props) {
        super(props);
        this.state = {count: props.initialCount};
        this.onClick = this.onClick.bind(this);
    }

    render() {
        return <div onClick={this.onClick}>{this.state.count}</div>;
    }

    onClick() {
        this.setState({count: this.state.count + 1});
    }
}

最后但同样重要的:

// 不要存储渲染不需要的状态,
// 它会导致不必要的重渲染和其它不一致性
// 反例

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

    render() {
        return <div>{this.state.count}</div>;
    }

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

        this.setState({interval});
    }

    componentWillUnmount() {
        clearInterval(this.state.interval);
    }
}

// 更优的实践

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

    render() {
        return <div>{this.state.count}</div>;
    }

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

    componentWillUnmount() {
        clearInterval(this._interval);
    }
}

setState( ) 是异步的吗?

基本上就是你调用 setState() 之后,React就准备更新,根据需要,计算会有一定的延迟。React 的文档没怎么提到这一点:

显然这两句是矛盾的。让我们做个实验,看看会发生什么?

class Component extends React.Component {
    constructor(props) {
        super(props);
        this.state = {count: 0};
        this.onClick = this.onClick.bind(this);
    }

    render() {
        return <div onClick={this.onClick}>{this.state.count}</div>;
    }

    onClick() {
        this.setState({count: this.state.count + 1});
        console.log(this.state.count);
    }
}

组件被渲染和交互后,你会看见控制台中打印的值是之前的状态。这是因为 React 保持了事件,它在得到足够的信息后才会批量提交更改。结果就是异步的状态变化。

但是,如果事件来源于外部会发生什么呢?

// 在同一个执行上下文中调用两次 setState() 是糟糕的做法。
// 此处为说明之用,正常应该使用原子的更新。

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

    render() {
        return <div>{this.state.count}</div>;
    }

    componentDidMount() {
        this._interval = setInterval(() => {
            this.setState({count: this.state.count + 1});
            console.log(this.state.count);
            this.setState({count: this.state.count + 1});
            console.log(this.state.count);
        }, 1000);
    }

    componentWillUnmount() {
        clearInterval(this._interval);
    }
}

即使在同样的执行上下文中调用了 setState() 两次,返回的还是已存在的值,正如文档所言。这是因为 React 没有得到批量更新的信息,不得不立即更新状态

很棘手,我建议你始终把 setState() 当做是异步的,免得惹麻烦。

我听说在特定的场景下 setState() 不会触发重渲染。是哪些场景?

  1. componentWillMount()componentWillRecieveProps() 中调用 setState(),不会触发任何额外的重渲染。React 会批量处理更新。
  2. shouldComponentUpdate() 返回 falserender()componentWillUpdate() 以及 componentDidUpdate() 一起被跳过了。

注意:

如果你想了解更多关于组件生命周期的事情,我也写过一篇相关文章


结论

正确地处理 React 中的 state 可以说是一项挑战。我希望你现在有了比较明确的认识。

如果你还有不懂的问题,请自由留言或者在 Twitter 上问我。我很高兴得到反馈,并会把问题追加到 FAQ 部分。

我希望本文能够让你对 React 有更加深入的认识。如此的话,请推荐给其他人。

相关文章