通过实例,学习编写 React 组件的“最佳实践”

Let's React

现在前端程序员都知道,React 是组件化的。当我开始学习 React 的时候,我记得当时已经存在了很多不同编写组件的方式了。如今,React
社区已经愈发成熟,但是对于组件正确编写姿势却没有一个相对完备的指导。

这篇文章仅从作者的观点出发,来谈一谈我们究竟应该如何来写高质量的 React 组件。

在开始前,需要说明以下几个问题:

  • 这篇文章以及代码实例,都采用了 ES6 或者 ES7 的写法;
  • 对于一些基本概念不再进行科普。适合有 React 初级经验的读者阅读;
  • 如果有任何问题,欢迎留言交流。

基于 Class 的组件最佳实践(Class Based Components)

基于 Class 的组件是状态化的,包含有自身方法、生命周期函数、组件内状态等。最佳实践包括但不限于以下一些内容:

1)引入 CSS 依赖 (Importing CSS)

我很喜欢 CSS in JavaScript 这一理念。在 React 中,我们可以为每一个 React 组件引入相应的 CSS 文件,这一“梦想”成为了现实。在下面的代码示例,我把 CSS 文件的引入与其他依赖隔行分开,以示区别:

import React, {Component} from 'react'
import {observer} from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

当然,这并不是真正意义上的 CSS in JS,具体实现其实社区上有很多方案。我的 Github 上 fork 了一份各种 CSS in JS 方案的多维度对比,感兴趣的读者可以点击这里

2)设定初始状态(Initializing State)

在编写组件过程中,一定要注意初始状态的设定。利用 ES6 模块化的知识,我们确保该组件暴露都是 “export default” 形式,方便其他模块(组件)的调用和团队协作。

import React, {Component} from 'react'
import {observer} from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

export default class ProfileContainer extends Component {
    state = { expanded: false }
    ......

3)设定 propTypes 和 defaultProps

propTypes 和 defaultProps 都是组件的静态属性。在组件的代码中,这两个属性的设定位置越高越好。因为这样方便其他阅读代码者或者开发者自己 review,一眼就能看到这些信息。这些信息就如同组件文档一样,对于理解或熟悉当前组件非常重要。

同样,原则上,你编写的组件都需要有 propTypes 属性。如同以下代码:

export default class ProfileContainer extends Component {
    state = { expanded: false }

    static propTypes = {
        model: React.PropTypes.object.isRequired,
        title: React.PropTypes.string
    }

    static defaultProps = {
        model: {
            id: 0
        },
        title: 'Your Name'
    }

Functional Components 是指没有状态、没有方法,纯组件。我们应该最大限度地编写和使用这一类组件。这类组件作为函数,其参数就是 props, 我们可以合理设定初始状态和赋值。

function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
    const formStyle = expanded ? {height: 'auto'} : {height: 0}
    return (
        <form style={formStyle} onSubmit={onSubmit}>
            {children}
        <button onClick={onExpand}>Expand</button>
        </form>
    )
}

4)组件方法(Methods)

在编写组件方法时,尤其是你将一个方法作为 props 传递给子组件时,需要确保 this 的正确指向。我们通常使用 bind 或者 ES6 箭头函数来达到此目的。

export default class ProfileContainer extends Component {
    state = { expanded: false }

    handleSubmit = (e) => {
        e.preventDefault()
        this.props.model.save()
    }

    handleNameChange = (e) => {
        this.props.model.changeName(e.target.value)
    }

    handleExpand = (e) => {
        e.preventDefault()
        this.setState({ expanded: !this.state.expanded })
    }

当然,这并不是唯一做法。实现方式多种多样,我专门有一片文章来对比 React 中对于组件 this 的绑定,可以点击此处参考。

5)setState 接受一个函数作为参数(Passing setState a Function)

在上面的代码示例中,我们使用了:

this.setState({ expanded: !this.state.expanded })

这里,关于 setState hook 函数,其实有一个非常“有意思”的问题。React 在设计时,为了性能上的优化,采用了 Batch 思想,会收集“一波” state 的变化,统一进行处理。就像浏览器绘制文档的实现一样。所以 setState 之后,state 也许不会马上就发生变化,这是一个异步的过程。

这说明,我们要谨慎地在 setState 中使用当前的 state,因为当前的state 也许并不可靠。
为了规避这个问题,我们可以这样做:

this.setState(prevState => ({ expanded: !prevState.expanded }))

我们给 setState 方法传递一个函数,函数参数为上一刻 state,便保证setState 能够立刻执行。

关于 React setState 的设计, Eric Elliott 也曾经这么喷过:setState() Gate,并由此展开了多方“撕逼”。作为围观群众,我们在吃瓜的同时,一定会在大神论道当中收获很多思想,建议阅读。

如果你对 setState 方法的异步性还有困惑,可以同我讨论,这里不再展开。

6)合理利用解构(Destructuring Props)

这个其实没有太多可说的,仔细观察代码吧:我们使用了解构赋值。除此之外,如果一个组件有很多的 props 的话,每个 props 应该都另起一行,这样书写上和阅读性上都有更好的体验。

export default class ProfileContainer extends Component {
    state = { expanded: false }

    handleSubmit = (e) => {
        e.preventDefault()
        this.props.model.save()
    }

    handleNameChange = (e) => {
        this.props.model.changeName(e.target.value)
    }

    handleExpand = (e) => {
        e.preventDefault()
        this.setState(prevState => ({ expanded: !prevState.expanded }))
    }

    render() {
        const {model, title} = this.props

        return ( 
            <ExpandableForm 
            onSubmit={this.handleSubmit} 
            expanded={this.state.expanded} 
            onExpand={this.handleExpand}>
                <div>
                    <h1>{title}</h1>
                    <input
                    type="text"
                    value={model.name}
                    onChange={this.handleNameChange}
                    placeholder="Your Name"/>
                </div>
            </ExpandableForm>
        )
    }
}

7)使用修饰器(Decorators)

这一条是对使用 mobx 的开发者来说的。如果你不懂 mobx,可以大体扫一眼。
我们强调使用 ES next decorate 来修饰我们的组件,如同:

@observer
export default class ProfileContainer extends Component {

使用修饰器更加灵活且可读性更高。即便你不使用修饰器,也需要如此暴露你的组件:

class ProfileContainer extends Component {
    // Component code
}
export default observer(ProfileContainer)

8)闭包(Closures)

一定要尽量避免以下用法:

<input
    type="text"
    value={model.name}
    // onChange={(e) => { model.name = e.target.value }}
    // ^ Not this. Use the below:
    onChange={this.handleChange}
    placeholder="Your Name"/>

不要:

onChange = {(e) => { model.name = e.target.value }}

而是:

onChange = {this.handleChange}

原因其实很简单,每次父组件 render 的时候,都会新建一个新的函数并传递给 input。
如果 input 是一个 React 组件,这会粗暴地直接导致这个组件的 re-render,需要知道,Reconciliation 可是 React 成本最高的部分。

另外,我们推荐的方法,会使得阅读、调试和更改更加方便。

9)JSX中的条件判别(Conditionals in JSX)

真正写过 React 项目的同学一定会明白,JSX 中可能会存在大量的条件判别,以达到根据不同的情况渲染不同组件形态的效果。
就像下图这样:

返例

这样的结果是不理想的。我们丢失了代码的可读性,也使得代码组织显得混乱异常。多层次的嵌套也是应该避免的。

针对于此,有很对类库来解决 JSX-Control Statements 此类问题,但是与其引入第三方类库的依赖,还不如我们先自己尝试探索解决问题。

此时,是不是有点怀念if...else?
我们可以使用大括号内包含立即执行函数IIFE,来达到使用 if...else 的目的:

解决思路

当然,大量使用立即执行函数会造成性能上的损失。所以,考虑代码可读性上的权衡,还是有必要好好斟酌的。
我更加建议的做法是分解此组件,因为这个组件的逻辑已经过于复杂而臃肿了。如何分解?请看我这篇文章。

总结

其实所谓 React “最佳实践”,想必每个团队都有自己的一套“心得”,哪里有一个统一套? 本文指出的几种方法未必对任何读者都适用。针对不同的代码风格,开发习惯,拥有自己团队一套“最佳实践”是很有必要的。从另一方面,也说明了 React 技术栈本身的灵活于强大。

另外,这篇文章并不是我原创,而是翻译了Our Best Practices for Writing React Components一文,并在此基础上进行了较大幅度扩展。

如果您对React生态有兴趣,同样推荐我的其他几篇文章:

Happy Coding!

PS:
作者Github仓库知乎问答链接
欢迎各种形式交流。

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

推荐阅读更多精彩内容

  • 本笔记基于React官方文档,当前React版本号为15.4.0。 1. 安装 1.1 尝试 开始之前可以先去co...
    Awey阅读 7,632评论 14 128
  • 最近看了一本关于学习方法论的书,强调了记笔记和坚持的重要性。这几天也刚好在学习React,所以我打算每天坚持一篇R...
    gaoer1938阅读 1,662评论 0 5
  • GUIDS 第一章 为什么使用React? React 一个提供了用户接口的JavaScript库。 诞生于Fac...
    jplyue阅读 3,504评论 1 11
  • 深入JSX date:20170412笔记原文其实JSX是React.createElement(componen...
    gaoer1938阅读 8,037评论 2 35
  • 空中飞舞的落叶,是风的追逐,还是树的抛弃…… 不知晓,或许过于痴情。用一生为树干支起了一片绿荫,甘愿为他忍受烈日的...
    LovLe阅读 281评论 0 2