写点什么

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:113579
用户头像

发布了 334 篇内容, 共 125.2 次阅读, 收获喜欢 834 次。

关注

评论

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

一个Hibernate的事务问题

YoungZY

hibernate

一个草根的日常杂碎(9月29日)

刘新吾

随笔杂谈 生活记录 社会百态

关于 UML 类图

西贝

UML 图表

中秋佳节,程序员教你AI三步成诗,秒变“李白”

华为云开发者社区

AI 中秋

区块链掀起全民创业热潮!数字资产是未来全球最具前景和价值的!

CECBC区块链专委会

区块链 数字货币

LeetCode题解:242. 有效的字母异位词,数组排序,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

国内上市进程或将提速!百度宣布小度科技独立融资

脑极体

架构师训练营 1 期 - 第三周 - 设计模式

三板斧

极客大学架构师训练营

单例模式

knight

GitHub上标星68k,基于SpringBoot+Netty分布式开源的即时通讯系统项目

Java架构之路

Java 程序员 编程语言 Netty 项目实战

技术解读丨目标检测之RepPoints系列算法

华为云开发者社区

算法 神经 目标检查

传统网络缺失货币层,比特币是否能担此大任?

blockchain

比特币 区块链 数字货币 比特币数字货币 区块俩金融

架构师训练营 - 作业 - 第二周

Max2012

数据库选型入门必读:如何在眼花缭乱的产品中挑出最适合业务的?

华为云开发者社区

数据库 数据库选择 关系型

全文!马云对数字时代全球化的全新解读

CECBC区块链专委会

全球化 数字时代

谈谈测试

得大自在

测试的价值 测试文化 测试落地

开源=免费?

Philips

开源 开源项目 开源代码 开源社区

软件架构(2)-框架设计

Zeke

极客大学架构师训练营

2020HC大会上,这群人在讨论云原生…

华为云开发者社区

华为 华为云 大会

阿里内部超流行的“SpringBoot+ 微服务指南”,理论与实战双管齐下

Java架构之路

Java 程序员 微服务 Spring Boot 编程语言

上手深度学习之前,我们先聊聊“数学”

华为云开发者社区

神经网络 学习 数学

架构师训练营第 1 期第 3 周学习总结

好吃不贵

极客大学架构师训练营

我一定是熬夜熬傻了,小程序后台获取用户信息居然发生了这件事

小Q

Java 小程序 学习 编程 架构

2020互联网公司中秋礼盒大比拼!(文末送福利)

Java架构师迁哥

太牛了,这份神仙级面试笔记把所有 Java 知识面试题都详解出来了

Java架构之路

Java 程序员 面试 编程语言

第三周学习代码重构总结

三板斧

极客大学架构师训练营

在vue2中使用ts

正经工程师

typescript vue.js

一周信创舆情观察(9.21~9.27)

统小信uos

Nexmark: 如何设计一个流计算基准测试?

Apache Flink

flink

极光无限:用AI赋能安全 解决安全行业人才紧缺难题

风向标

人工智能

剖析Java15新语法特性

九叔

Java 架构 Java 分布式 java15新特性

4月17日 HarmonyOS 开发者日·上海站

4月17日 HarmonyOS 开发者日·上海站

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