10 月,开发者不可错过的开源大数据大会-2021 WeDataSphere 社区大会深圳站 了解详情
写点什么

MuseFind:编写 React 组件的最佳实践

2017 年 3 月 01 日

React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,就在 2013 年 5 月开源了。

由于 React 的设计思想极其独特,属于革命性创新,性能出众,代码逻辑却非常简单。所以,越来越多的人开始关注和使用,认为它可能是将来 Web 开发的主流工具。

来自 MuseFind 的 Scott Domes 日前写了一篇文章,阐述了他们编写React 组件的最佳实践。Scott Domes 是MuseFind 的前端移动开发工程师。

经作者授权,InfoQ 翻译并分享本文。以下是正文:


当我第一次开始写React 代码时,我记得看到过许多不同的编写组件的方法,各教程之间有很大的不同。虽然自那时以来框架已经相当成熟,但似乎还没有一个确定“正确”编写的方式。

在过去一年里,在 MuseFind ,我们的团队编写了很多 React 组件。我们已经逐渐完善了方法,直到我们满意为止。

本指南代表我们建议的最佳做法。我们认为本文对新手和老手都有所帮助。

阅读本文之前,读者需要注意以下几点:

  • 我们使用 ES6 和 ES7 语法。
  • 如果你不确定展示型组件和容器组件之间的区别,我们建议你先阅读这篇文章
  • 如果您有任何建议问题或反馈,请在原文的评论区中告诉我们。

基于类的组件

基于类的组件是有状态的,或许还包含方法。我们尽可能少地使用它们,但它们也有自己的位置。

让我们逐行构建我们的组件。

导入 CSS

复制代码
import React, {Component} from 'react'
import {observer} from 'mobx-react'
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

理论上,我喜欢 CSS in JavaScript 。但它仍然是一个新的想法,还没有出现一个成熟的解决方案。在此之前,我们将一个 CSS 文件导入到每个组件。

我们还通过换行将依赖导入与本地导入分开。

初始化状态

复制代码
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 }

如果你使用 ES6(ES7 不适用),在构造函数中初始化状态。否则,使用专用于 ES7 的方法。更多信息在这篇文章

我们还要确保将我们的类导出为默认类。

propTypes 和 defaultProps

复制代码
propTypes 和 defaultProps
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 }
static propTypes = {
model: React.PropTypes.object.isRequired,
title: React.PropTypes.string
}
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}

propTypes 和 defaultProps 是静态属性,在组件代码中声明的优先级尽可能高。由于它们作为文档,因此它们应该对其他读取文件的开发者可见。

所有的组件应该有 propTypes。

方法

复制代码
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 }
static propTypes = {
model: React.PropTypes.object.isRequired,
title: React.PropTypes.string
}
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}
handleSubmit = (e) => {
e.preventDefault()
this.props.model.save()
}
handleNameChange = (e) => {
this.props.model.name = e.target.value
}
handleExpand = (e) => {
e.preventDefault()
this.setState({ expanded: !this.state.expanded })
}

使用类组件,当将方法传递给子组件时,必须确保它们在调用时具有正确的 this。通常通过传递 this.handleSubmit.bind(this) 到子组件来实现。

我们认为这种方法更简洁也更容易,通过 ES6 的箭头函数自动保持正确的上下文。

解构 props

复制代码
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 }
static propTypes = {
model: React.PropTypes.object.isRequired,
title: React.PropTypes.string
}
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}
handleSubmit = (e) => {
e.preventDefault()
this.props.model.save()
}
handleNameChange = (e) => {
this.props.model.name = 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>
)
}
}

具有多个 props 的组件,每个 props 应该占据单独一个行,如上所示。

装饰器

复制代码
@observer
export default class ProfileContainer extends Component {

如果你使用像 mobx 这样的东西,你可以将类组件装饰成这样:这与将组件传递到函数相同。

装饰器通过灵活、可读的方式来修改组件功能。我们广泛地使用装饰器,配合mobx 和我们自己的 mobx-models 库。

如果您不想使用装饰器,请执行以下操作:

复制代码
class ProfileContainer extends Component {
// Component code
}
export default observer(ProfileContainer)

闭包

避免传递新的闭包到子组件,像这样:

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

这就是为什么每次父组件渲染时,创建一个新的函数并传递给输入的原因。

如果输入是 React 组件,这将自动触发它重新渲染,而不管它的其他 props 是否实际改变。

调和算法(Reconciliation)是 React 最耗时的部分。不要让它比所需更难!此外,传递类的方法更容易阅读、调试和更改。

这是我们的完整组件:

复制代码
import React, {Component} from 'react'
import {observer} from 'mobx-react'
// Separate local imports from dependencies
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'
// Use decorators if needed
@observer
export default class ProfileContainer extends Component {
state = { expanded: false }
// Initialize state here (ES7) or in a constructor method (ES6)
// Declare propTypes as static properties as early as possible
static propTypes = {
model: React.PropTypes.object.isRequired,
title: React.PropTypes.string
}
// Default props below propTypes
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}
// Use fat arrow functions for methods to preserve context (this will thus be the component instance)
handleSubmit = (e) => {
e.preventDefault()
this.props.model.save()
}
handleNameChange = (e) => {
this.props.model.name = e.target.value
}
handleExpand = (e) => {
e.preventDefault()
this.setState(prevState => ({ expanded: !prevState.expanded }))
}
render() {
// Destructure props for readability
const {
model,
title
} = this.props
return (
<ExpandableForm
onSubmit={this.handleSubmit}
expanded={this.state.expanded}
onExpand={this.handleExpand}>
// Newline props if there are more than two
<div>
<h1>{title}</h1>
<input
type="text"
value={model.name}
// onChange={(e) => { model.name = e.target.value }}
// Avoid creating new closures in the render method- use methods like below
onChange={this.handleNameChange}
placeholder="Your Name"/>
</div>
</ExpandableForm>
)
}
}

函数组件

这些组件没有状态、方法。它们是纯粹的,简单的。因此要尽可能经常使用它们。

propTypes

复制代码
propTypes
import React from 'react'
import {observer} from 'mobx-react'
import './styles/Form.css'
const expandableFormRequiredProps = {
onSubmit: React.PropTypes.func.isRequired,
expanded: React.PropTypes.bool
}
// Component declaration
ExpandableForm.propTypes = expandableFormRequiredProps

这里,我们在组件声明前分配 propTypes,因此它们立即可见。在组件声明下面,我们正确地分配它们。

解构 Props 和 defaultProps

复制代码
import React from 'react'
import {observer} from 'mobx-react'
import './styles/Form.css'
const expandableFormRequiredProps = {
onSubmit: React.PropTypes.func.isRequired,
expanded: React.PropTypes.bool
}
function ExpandableForm(props) {
return (
<form style={props.expanded ? {height: 'auto'} : {height: 0}}>
{props.children}
<button onClick={props.onExpand}>Expand</button>
</form>
)
}

我们的组件是一个函数,它的 Props 作为其参数。我们可以这样扩展:

复制代码
import React from 'react'
import {observer} from 'mobx-react'
import './styles/Form.css'
const expandableFormRequiredProps = {
onExpand: React.PropTypes.func.isRequired,
expanded: React.PropTypes.bool
}
function ExpandableForm({ onExpand, expanded = false, children }) {
return (
<form style={ expanded ? { height: 'auto' } : { height: 0 } }>
{children}
<button onClick={onExpand}>Expand</button>
</form>
)
}

注意,我们也可以使用默认参数作为 defaultProps 以高度可读的方式。如果展开没有定义的话,我们将其设置为 false。(这是一个有点强迫的例子,因为它是一个布尔,但非常有用,以避免“无法读取未定义”错误与对象)。

避免使用以下 ES6 语法:

复制代码
const ExpandableForm = ({ onExpand, expanded, children }) => {

看起来很现代,但此处的函数实际上是未命名的。

如果你的 Babel 设置正确,这个名字的缺失不会成为一个问题:但如果不是,任何错误将 < > 中显示,对调试而言,是一个非常严重的问题。

未命名的函数也可能导致 Jest(一个 React 测试库)的问题。由于潜在的难以理解的 bug,以及并没有什么真正的好处,我们建议使用 function 而不是 const。

包装

因为你不能使用装饰器和功能组件,你只需将函数作为参数传递给它:

复制代码
import React from 'react'
import {observer} from 'mobx-react'
import './styles/Form.css'
const expandableFormRequiredProps = {
onExpand: React.PropTypes.func.isRequired,
expanded: React.PropTypes.bool
}
function ExpandableForm({ onExpand, expanded = false, children }) {
return (
<form style={ expanded ? { height: 'auto' } : { height: 0 } }>
{children}
<button onClick={onExpand}>Expand</button>
</form>
)
}
ExpandableForm.propTypes = expandableFormRequiredProps
export default observer(ExpandableForm)

这是我们的完整组件:

JSX 条件

很可能你要做很多条件渲染。这里是你想避免的地方:

(点击放大图像)

这是我在MuseFind 早期写的实际代码,饶恕我吧。

不,嵌套的三元运算并不是一个好主意。

有一些库解决了这个问题( JSX 控制语句),但是,而不是引入另一个依赖,我们解决了这种应对复杂条件的方法:

(点击放大图像)

以上所示是重构版本。

使用花括号括起一个 IIFE ,然后把你的 if 语句置于里面,返回任何你想要的渲染。请注意,这样的 IIFE 可能会导致性能下降,但在大多数情况下,它不会严重到以致失去可读性。

此外,当你只想渲染一个条件上的元素,而不是这样做:

复制代码
{
isTrue
? <p>True!</p>
: <none/>
}

使用 short-circuit 赋值:

复制代码
{
isTrue &&
<p>True!</p>
}

感谢王下邀月熊对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017 年 3 月 01 日 16:113677
用户头像

发布了 340 篇内容, 共 132.0 次阅读, 收获喜欢 864 次。

关注

评论

发布
暂无评论
发现更多内容

揭秘开源项目 Apache Pulsar 如何挑战 Kafka

Apache Pulsar

kafka 开源 云原生 Apache Pulsar 消息中间件

使用Spring Cloud Stream玩转RabbitMQ,RocketMQ和Kafka

Barry的异想世界

kafka RocketMQ RabbitMQ 消息队列 spring cloud stream

架构训练营-week2-作业

于成龙

作业 架构训练营

架构师训练营第二周作业

文智

极客大学架构师训练营

MySQL是如何实现可重复读的?

超超不会飞

MySQL

编程语言的本质

张荣召

第二周

等燕归

架构训练营 - 第 2周课后作业 - 学习总结

Pudding

Java中的遍历(遍历集合或数组的几种方式)

keaper

Java List 遍历

架构师训练营 - 第 2 周课后作业(1 期)

Pudding

[架构师训练营第1期]第二周学习总结

猫切切切切切

极客大学架构师训练营

面向对象设计原则----里氏替换原则(LSP)

张荣召

案例分析--反应式编程框架Flower的设计

张荣召

OOA-OOD:面向对象分析/设计练习

张荣召

面向对象设计原则--开放关闭原则(OCP)

张荣召

面向对象设计原则----依赖倒置原则(DIP)

张荣召

graylog日志分析系统上手教程

MySQL从删库到跑路

Apache Linux 运维 日志分析 实时 Web 日志分析器

2.框架设计-依赖倒置原则,接口隔离原则

博古通今小虾米

架构师训练营第二周学习总结

张荣召

面向对象设计原则----单一职责原则(SRP)

张荣召

面向对象设计原则----接口分离原则(ISP)

张荣召

第二周学习框架设计总结

三板斧

极客大学架构师训练营

基于 iOS14 系统的游戏卡顿问题解决方案

白开水

typescript 游戏开发 iOS14 游戏卡顿 ios开发

#第二周作业

vitaminc

深入理解JVM垃圾回收算法 - 标记整理算法

WANDEFOUR

标记整理 双指针算法 Lisp2 引线整理算法

金融科技推进数字金融“新基建”,着力建设三种类型数字金融基础设施

CECBC区块链专委会

金融 科技 科技革命

国内首个区块链村正式落地:数字经济的裂变之路

CECBC区块链专委会

区块链 数字经济

第二周

scorpion

作业-2020-09-27

芝麻酱

第二周总结

等燕归

第二周课后练习

薛凯

MuseFind:编写React组件的最佳实践-InfoQ