React 技巧和最佳实践

React 技巧和最佳实践

不管如何,你都应该去尝试一下当下炙手可热的 React。这篇文章我会总结一些关于 React 的技巧和最佳实践。

使用 PureComponent

React 在 15.3 版本中被引入了类 PureComponent,表明之前社区中奖 shouldComponentUpdatePureRenderMixin作为一种最佳实践是一种共识。

当然两点需要简单说明的地方:

首先,你需要了解的是当 state 改变时,React 是如何更新我们的组件的。简单的说,当一个组件的状态改变时,它以及它所有后代的组件的 render 方法都会被调用一次,然后生成一个虚拟的 DOM 树再将它与内存中的虚拟的 DOM 树进行比较,然后采用最小代价的操作来更新浏览器中真实 DOM。因此,当我们通过 setState 方法更新状态的时候,React 看起来像是更新了整个 DOM 树。shouldComponentUpdate 提供了一种我们可以侵入到这个过程的能力,当该方法返回 false 的时候,该组件的 render 方法不会被调用,不会产生新的虚拟 DOM,该组件自然也没有后续虚拟 DOM 的 diff 和真实 DOM 更新。

其次,PureRenderMixin 只是浅比较 state 和 props ,只意味着你的 render 在相同的 state 和 props 的情况下必须要返回一致的输出,当你使用了复杂的数据类型来作为 state 和 prop 的时候,你可能需要引入 Immutable.js 这样的不可变数据类型的库。

通常情况下,它能够大幅提高你的应用的性能,尤其是你在构建一个复杂的应用的时候。当你使用类似于 Redux 这样的库用于集中管理你应用的 state 时,数据流总是从顶部单向流向各个层级的组件,因此意味着每一次 store 的变化,都会导致整个组件树中所有组件的 render 方法被调用一次,这也意味着引入 PureComponent 能够避免那些不必要的 render 方法被调用。

状态设计和无状态组件

关于状态的设计有些时候并不是一目了然,尤其是对新手来说。但是总结起来只有一条核心的原则,那就是尽量保证你的组件无状态 。这会让你的组件更容易理解和维护,也会让你整个应用的数据流更加清晰。

具体可以拆分为两条具体的原则,状态最小子集状态集中管理 。状态最小集要求我们找到影响视图变化的状态的最小集合,而状态集中管理则要求我们使用类似 redux 这样的状态容器来集中管理我们的状态。实践中具体的技巧大概有以下 3 条:

消除组件内重复的状态

其核心是:

任何可以被计算或推导的数据不应该作为状态。

如果将可以被计算和推导出来的数据作为状态,那么就意味着你要维护和保持这些状态之间的同步,这通常是 bug 产生的地方,而且排查起来也很不容易。

消除组件间重复的状态

其核心是:

_ 任何除自身以及后代以外的组件会关注的数据不应该作为自身的状态_。

也就是通常提到的「向上顶」的原则。

这一条没有上一条那么直观,但却是很容易犯错的地方。任何除自身以及后代以外的组件通常意外着父组件或者其兄弟组件。

我举一个简单的例子,现在你要设计一个筛选器组件,它会涉及到很多筛选项,然后当用户提交的时候,页面下方的列表会更新。我们会很自然的将当前用户筛选的值作为该组件内部的一个状态,由该组件内部跟新和维护,然后在用户点击的确定的时候,通过暴露一个 onSubmit 事件,将参数传递给父组件,如组件更新列表。类似如下:

class Filter extends React.Component {
    constructor(props) {
        super(props)

        this.state = {
            foo: props.defaultFoo,
            bar: props.defaultBar
        }
    }

    handleBarChange(newBar) {
        this.setState({
            bar: newBar
        })
    }

    handleFooChange(newFoo) {
        this.setState({
            foo: newFoo
        })
    }

    handleSubmit() {
        const { bar, foo } = this.state
        this.props.onSubmit({ bar, foo })
    }

    render () {
        //...
    }
}

这样做会带来很多问题,比如如组件如果还接受其他的筛选条件来更新列表,比如翻页,那么意味这父组件必须要在 onSubmit 事件中不得不保存该筛选条件,可能将它存到 state 里面,那就意味同一份数据在父组件中也保存了一份相同的引用,同时意味着你要时刻小心的保持两者的同步。再比如,我现在用一个弹窗或者下拉框的形式来展现这些筛选条件,也就是说,但用户筛选完成之后,如果并没有点击提交,而是直接关闭了弹窗。那么当用户再次点开弹窗的时候,你需要重置会已生效的筛选条件,这个时候你又不得不将 props 同步给 state。

接下来我们进行改进,筛选项的值父组件是会关注的,所以「向上顶」放到父组件里面去。大概如下:

class Filter extends React.Component {
    constructor(props) {
        super(props)
    }

    handleBarChange(newBar) {
        const {value, onChange} = this.props

        onChange(Object.assign({}, value, {bar: newBar}))
    }

    handleFooChange(newFoo) {
        const {value, onChange} = this.props

        onChange(Object.assign({}, value, {foo: newFoo}))
    }

    handleSubmit() {
        this.props.onSubmit()
    }

    render () {
        //...
    }
}

然后 Filter 组件变得很清晰明了,仅仅负责将父组件传入的筛选项信息 value 正确的渲染,同时暴露 onChange 和 onSubmit 父组件,将筛选状态的维护工作拆分给了父组件。而我上面提到的 2 个问题也就迎刃而解了。

通过以上 2 条技巧,我们很容易消除那些重复的状态,保证我们应用处于最小状态集合中。同时能够衍生出一条简单的准则帮助你发现问题,那就是当你发现你在维护两个状态的同步时,通常意味着你的组件设计有问题,如果两个状态在同一个组件内,参考技巧 1,否则参考技巧 2。

现在保证了状态最小集合,但要达到无状态组件,还需要我们将这些状态移除我们的组件,也就是接下来第 3 条技巧。

使用状态容器管理状态

这一条技巧很简单,就是使用例如 redux 这样的状态容器来集中管理我们应用中的状态,尤其是在构建复杂的应用的时候。关于 redux 的使用,社区里面有很多相关的教程,这里不再赘述。

最后,值得一提的是,我们是否应该将所有的状态都移除组件,达到真正的无状态组件。答案是否定的,因为这样不仅将会导致实践中流程极其繁琐,甚至某些时候也是不必要的。

但某些时候,又是什么时候?

简单的想一下我们浏览器自带的 select 元素,选择器是否展开必然应该作为一个状态。但是我们没有需要将其作为一个属性传给元素来控制元素是否展开,而且 select 元素确实也没有这个属性。因此,这个原则就是:

当某个状态之后该组件自身会关注,其余组件(父类和子类等等)都不关注的时候,那么你可以将它作为状态保留到组件内部。

关注 ComponentDidUpdate

最后,我想说明一下 ComponentDidUpdate 这个方法,这个方法经常被大家忽略,但其实应该有着更大的用武之地。

对于 React 而言,理想情况下应该是,我们设计好状态以及状态和视图的关联关系以后,在随后时间推移和用户交互的过程中我们只需要更改我们的状态就可以了。但实际情况是,我们很多时候在更改状态(例如,筛选项等)之后还需要去服务器上请求相应的数据。我们最常见的做法是在 setState 的回调函数里,如果使用了 类似于 redux 的状态管理容器时,我们可能派发一个异步 Action。

// setState 回调
function handlePageChange(newPage) {
    this.setState(
        { page: newPage },

        () => this.fetchData()
    );
}


// dipatch 异步的 action
function fetchDataWithPageChange(page) {
    return async (dispatch, getState) => {
        // 同步的 action
        dispatch(pageChange(page));

        const query = getState().query;

        // 异步操作
        let res = await get(url, Object.assign({ page }, query));

        // 同步 action
        dispatch(dataChange(res));
    }
}

这样做最大的问题是,我们将我们改变状态的行为和数据请求耦合在了一起,而当其他同样需要请求相同数据的状态改变之后,我们也必须记着调用相同的数据请求的方法,需要小心翼翼,不能遗忘。

如果我们把状态变化与数据请求的关系写在 componentDidUpdate 里面,那么我们就能够专注的更新我们的状态,因为状态更新会自动触发数据请求。看到了吗?这个我们处理状态和视图的关系如此相似,一旦指定了映射关系,那么只需要简单更新状态,就能自动触发另一方相应的动作。

改进如果下:

class Com extends PureComponent {
    // ...
    handlePageChange(newPage) {
        this.setState({
            page: newPage
        });
    }

    componentDidUpdate(preProps, preState) {
        const { page } = preState;

        if (page !== this.state.page) {
            this.fetchData();
        }
    }
    // ...
}

// 或者
class Com extends PureComponent {
    // ...
    handlePageChange(newPage) {
        this.props.dispatch(changePage(newPage));
    }

    componentDidUpdate(preProps) {
        const { page } = preProps;

        if (page !== this.props.page) {
            this.props.dispatch(fetchData());
        }
    }
    // ...
}

function changePage(page) {
    return {
        type: 'CHANGE_PAGE',
        index: page
    };
}

总结

关于 React 的总结和最佳实践暂时想到的就这些,从使用了 React 之后,在我需要构建一个在线的应用之前(不管是否使用 React ),我都会先首先梳理页面视图随着时间推移和用户的交互时是如何变化的,具体到 React 就是整个 app 的数据流。它们就像是一栋建筑的设计稿和结构图,等到你深入其中的细枝末节的时候仍然给能够对整体了然于胸,不会被一叶蔽目。我很享受这个过程,希望你们也能喜欢。

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

推荐阅读更多精彩内容