ReactJS组件state的最佳实践

当我们写React应用的时候,知道在组件中何时使用state何时不使用state,是非常重要的。在这篇文章中,我将回顾我所认为的使用state的最佳实践:

  1. 如果component没有自己的数据,那么其他数据便不应该影响它的state。
  2. 用于描述组件的state尽可能简单。
  3. 运算和条件判断移动到render函数。

这些规则如果有特殊情况,应该在适当的时候违反。不过如果你能够一直都遵循它们的话,你会发现你的component更容易解耦,测试更容易写,而且整个应用的bug也很少。下面让我们仔细看看这些规则:

1. 如果component没有自己的数据,那么其他数据便不应该影响它的state

第一,可能是最重要的一点,component的state不应该依赖于props传递。当然props可能向子组件传递state,例如,在一个普通的input组件中,为了禁用input的文字输入,我可能选择一个disabled的prop。但是当我说'state'的时候,我是明确的指component的state属性。所以,当state开始依赖于它的props的时候,你可能会发现这是一段不好的代码。看看以下代码片段:

    import React from 'react';

    class UserWidget extends React.Component {

    // BAD: 通过props接收到的值设置this.state.fullName
        constructor (props) {
            this.state = {
                fullName: `${props.firstName} ${props.lastName}`
            };
        }

        render () {
            var fullName = this.state.fullName;
            var picture = this.props.picture;

            return (
                <div>
                    <img src={picture} />
                    <h2>{fullName}</h2>
                </div>
            );
        }
    }

以上代码有什么问题?一开始可能不是很明显,但是假如firstName或者lastName改变了,UserWidget组件的视图将不会改变。构造函数在组件初始化渲染执行之后只会调用一次,因此fullName的值永远是第一次渲染时候的值。React新手可能经常会犯这样的错误,因为setState是更新组件视图的最简单且最明显的方式。

你应该问问你自己,该组件是否拥有这些数据,内部的firstName和lastName创建了吗?如果没有,那么state不应该依赖便不应该依赖于这些数据。那么最好的避免这个问题的方式是什么呢?在render函数里面计算fullName的值。

    render () {
        var fullName = `${this.props.firstName} ${this.props.lastName}`;
        // ...
    }

把fullName移动到render函数里面之后,我们将不用再关心fullName的值是否更新了。当props改变的时候,React会运行一个钩子函数--componentWillReceiveProps,然而我还是会考虑这种反模式,因为它不需要增加项目的复杂性。

当然,如果你在组件初始化之后不关心props,那么这条规则将不会适用。

当使用React.createClass代替extends React.Component时候,则用getInitialState代替constructor。
有时候,"state"将需要设置一些值,在flux模式中,可能是根控制器组件监听不同的stores。

2. 用于描述组件的state尽可能简单

你应该尽可能的简单的去描述一个组件的状态。在很多种情况下,这意味着用布尔值是更好的方式。

思考下面的例子,我们有一些组件,它们在state里面的class属性是基于clicked和hovered事件改变的。(不管你信不信,我看到过很多这样的例子)

    import React from 'react';
    var cx = React.addons.classSet;

    class ArbitraryWidget extends React.Component {

        constructor() {
            this.state = {
                classes: []
            };
        }

        // BAD: 当鼠标滑过的时候,把'hover'push到this.state.classes
        handleMouseOver() {
            var classes = this.state.classes;
            classes.push('hover');

            this.setState({ classes: classes });
        }

        // BAD: 当鼠标离开的时候,从this.state.classes移除'hover'
        handleMouseOut() {
            var classes = this.state.classes;
            var index = classes.indexOf('hover');
            classes.splice(index, 1);

            this.setState({ classes: classes });
        }

        // BAD: 被点击的时候,在this.state.classes切换'active'
        handleClick() {
            var classes = this.state.classes;
            var index = classes.indexOf('active');

            if (index != -1) {
                classes.splice(index, 1);
            } else {
                classes.push('active');
            }

            this.setState({ classes: classes });
        }

        render() {
            var classes = this.state.classes;

            return ( 
                <div className={cx(classes)}
                    onClick={this.handleClick.bind(this)}
                    onMouseOver={this.handleMouseOver.bind(this)}
                    onMouseOut={this.handleMouseOut.bind(this)}
                />
            )
        }
    }

这个组件可以运行,但是我持保留意见。它现在的state是一个存着字符串类型的数组,this.state.classes = ['active', 'hover'],不仅代码的可读性很差,而且改变起来特别麻烦。假如有其他组件依赖于我的这个class的数组,那么查看这个数组是否包含hover肯定比查看hover的布尔值是什么的难度要大。我们需要重构这段代码,用布尔值代表组件是否应该有这些class,例如isHovering === true意味着我是否应该使用hover这个class。

    import React from 'react';
    var cx = React.addons.classSet;

    class ArbitraryWidget extends React.Component {

        constructor() {
            this.state = {
                isHovering: false,
                isActive: false
            };
        }

        // GOOD: 当鼠标滑过的时候,this.state.isHovering设置为true
        handleMouseOver() {
            this.setState({ isHovering: true });
        }

        // GOOD: 当鼠标离开的时候,this.state.isHovering设置为false
        handleMouseOut() {
            this.setState({ isHovering: false });
        }

        // GOOD: 被点击的时候,改变this.state.active
        handleClick() {
            var active = !this.state.isActive;
            this.setState({ isActive: active });
        }

        render() {
            // use the classSet addon to concat an array of class names together
            var classes = cx([
                this.state.isHovering && 'hover',
                this.state.isActive && 'active'
            ]);

            return ( 
                <div className={cx(classes)}
                    onClick={this.handleClick.bind(this)}
                    onMouseOver={this.handleMouseOver.bind(this)}
                    onMouseOut={this.handleMouseOut.bind(this)}
                />
            );
        }
    }

为了使用这些state的布尔值,我们必须在render函数里面计算class数组。但是,我们增强了代码的可读性,this.state.isHovering远比this.state.classes.indexOf('hover') != -1更能代表组件实际的状态。这个组件更容易扩展和测试,因为我们不需要考虑数组的构建。

我想再说一遍,你应该始终以用最简单的方式表示state为目标。这并不一定意味着你只能存储布尔值,有可能是深层嵌套的对象,也可能是数字、字符串或者函数。
想象一下作为其他人,试图观察组件返回的一个class的数组的状态,这个数组对于你是否有用呢?当然没有。相比之下布尔值isActive是更为可行的。我希望你明白我的意思。

3. 运算和条件判移动到render函数

在前面的两条规则中,这一条其实已经提到了。然而,它仍然是值得注意的。尽可能的在render函数中进行最后一步运算。虽然这样也许会略慢于其他方法,但它能确保最少的重定向组件,在轻微的性能提升之前,我们应该更注重代码的可读性和扩展性。

我需要连接prop中的firstName和lastName?把它移动到render函数。我的组件需要使用哪个class?在render函数中做决定。如果我的todo列表没有任何项目,我应该显示在text框中显示一个placeholder?在render函数中做决定。我需要格式化电话号码?在render函数中做决定。我该如何呈现出子组件?在render函数中做决定。我今天要吃午饭吗?在render函数中做决定。

当然,你不要把所有代码都放在一个函数里面。相反,最好把它们分割成合适的helper函数(用一个好的名字),关键是你用render函数做太多的事情的话,应该减少它的复杂性。你可以用一个前缀来表示helper函数。例如:

    // GOOD: Helper function to render fullName
    renderFullName () {
        return `${this.props.firstName} ${this.props.lastName}`;
    }

    render () {
        var fullName = this.renderFullName();
        // ...
    }

CPU密集运算

因为我建议你把所有的东西都推迟到render函数中,它会导致CPU密集运算也会推迟。为了避免重复复杂的渲染,考虑memoization的功能。

不要把变量存储到component实例上

不要像下面这样做:

    class ArbitraryWidget extends React.Component {

        constructor () {
            this.derp = 'something';
        }

        handleClick () {
            this.derp = 'somethingElse';
        }

        render () {
            var something = this.derp;
        }
    }

这是非常不好的,不仅是因为你没有遵守用this.state存储值的约定,而且this.derp改变的时候,不会自动触发render。

原文地址
Best Practices for Component State in React.js

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354

推荐阅读更多精彩内容