资讯专栏INFORMATION COLUMN

写 React 组件的最佳实践

lakeside / 1298人阅读

摘要:本文介绍了,我们团队写组件的最佳实践。这样可以避免类似之类的错误避免使用函数表达式的方式来定义组件,如下这看起来非常酷,但是在这里,通过函数表达式定义的函数却是匿名函数。匿名函数也可能会导致测试库出问题。

本文为译文,已获得原作者允许,原文地址:http://scottdomes.com/blog/ou...

当我第一次开始写 React 时,我发现多少个 React 教程,就有多少种写 React 组件方法。虽然如今,框架已经成熟,但是并没有一个 “正确” 写组件的方法。

在 MuseFind 的一年以来,我们的团队写了大量的 React 组件。我们精益求精,不断完善写 React 组件的方法。

本文介绍了,我们团队写 React 组件的最佳实践。
我们希望,无论你是初学者,还是经验丰富的人,这篇文章都会对你有用的。

在开始介绍之前,先说几个点:

我们团队使用 ES6 和 ES7 的语法。

如果不清楚表现组件(presentational components)和容器组件(container components)之间的区别,我们建议先阅读 这篇文章。

如果有任何建议,问题或反馈意见,请在评论中通知我们。

基于类的组件

基于类的组件(Class based components)是包含状态和方法的。
我们应该尽可能地使用基于函数的组件(Functional Components
)来代替它们。但是,现在让我们先来讲讲怎么写基于类的组件。

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

引入 CSS
import React, { Component } from "react"
import { observer } from "mobx-react"

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

我认为最理想的 CSS 应该是 CSS in JavaScript。但是,这仍然是一个新的想法,还没有一个成熟的解决方案出现。
所以,现在我们还是使用将 CSS 文件引入到每个 React 组件中的方法。

我们团队会先引入依赖文件(node_modules 中的文件),然后空一行,再引入本地文件。

初始化状态
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 }

可以使用在 constructor 中初始化状态的老方法。
也可以使用 ES7 这种简单的初始化状态的新方法。
更多,请阅读 这里。

propTypes and defaultProps
import React, { Component } from "react"
import { observer } from "mobx-react"
import { string, object } from "prop-types"

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

export default class ProfileContainer extends Component {
  state = { expanded: false }
 
  static propTypes = {
    model: object.isRequired,
    title: string
  }
 
  static defaultProps = {
    model: {
      id: 0
    },
    title: "Your Name"
  }

propTypesdefaultProps 是静态属性(static properties),在组件代码中,最好把它们写在组件靠前的位置。当其他开发人员查看这个组件的代码时,应该立即看到 propTypesdefaultProps,因为它们就好像这个组件的文档一样。(译注:关于组件书写的顺序,参考 这篇文章)

如果使用 React 15.3.0 或更高版本,请使用 prop-types 代替 React.PropTypes。使用 prop-types 时,应当将其解构。

所有组件都应该有 propTypes

Methods
import React, { Component } from "react"
import { observer } from "mobx-react"
import { string, object } from "prop-types"

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

export default class ProfileContainer extends Component {
  state = { expanded: false }
 
  static propTypes = {
    model: object.isRequired,
    title: string
  }
 
  static defaultProps = {
    model: {
      id: 0
    },
    title: "Your Name"
  }
  
  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 })
  }

使用基于类的组件时,当你将方法传递给组件时,你必须保证方法在调用时具有正确的上下文 this。常见的方法是,通过将 this.handleSubmit.bind(this) 传递给子组件来实现。

我们认为,上述方法更简单,更直接。通过 ES6 箭头功能自动 bind 正确的上下文。

setState 传递一个函数

在上面的例子中,我们这样做:

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

因为 setState 它实际上是异步的。
由于性能原因,所以 React 会批量的更新状态,因此调用 setState 后状态可能不会立即更改。

这意味着在调用 setState 时,不应该依赖当前状态,因为你不能确定该状态是什么!

解决方案是:给 setState 传递函数,而不是一个普通对象。函数的第一个参数是前一个状态。

this.setState(prevState => ({ expanded: !prevState.expanded }))
解构 Props
import React, { Component } from "react"
import { observer } from "mobx-react"
import { string, object } from "prop-types"
import ExpandableForm from "./ExpandableForm"
import "./styles/ProfileContainer.css"
export default class ProfileContainer extends Component {
  state = { expanded: false }
 
  static propTypes = {
    model: object.isRequired,
    title: string
  }
 
  static defaultProps = {
    model: {
      id: 0
    },
    title: "Your Name"
  }
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 ( 
      
        

{title}

) } }

如上,当组件具有多个 props 值时,每个 prop 应当多带带占据一行。

装饰器
@observer
export default class ProfileContainer extends Component {

如果使用 mobx,那么应当是用装饰器(decorators)。其本质是将装饰器的组件传递到一个函数。

使用装饰器一种更加灵活和更加可读的方式。
我们团队在使用 mobx 和我们自己的 mobx-models 库时,使用了大量的装饰器。

如果您不想使用装饰器,也可以按照下面的方式做:

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

避免传递一个新闭包(Closures)给子组件,像下面这样:

 { model.name = e.target.value }}
// ^ 上面是错误的. 使用下面的方法:
onChange={this.handleChange}
placeholder="Your Name"/>

为什么呢?因为每次父组件 render 时,都会创建一个新的函数(译注:通过 (e) => { model.name = e.target.value } 创建的新的函数也叫 闭包)。

如果将这个新函数传给一个 React 组件,无论这个组件的其他 props 有没有真正的改变,都就会导致它重新渲染。

调和(Reconciliation)是 React 中最耗费性能的一部分。因此,要避免传递新闭包的写法,不要让调和更加消耗性能!另外,传递类的方法的之中形式更容易阅读,调试和更改。

下面是我们整个组件:

import React, { Component } from "react"
import { observer } from "mobx-react"
import { string, object } from "prop-types"
// 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: object.isRequired,
    title: 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 ( 
      
        // Newline props if there are more than two
        

{title}

{ model.name = e.target.value }} // Avoid creating new closures in the render method- use methods like below onChange={this.handleNameChange} placeholder="Your Name"/>
) } }
基于函数的组件

基于函数的组件(Functional Components)是没有状态和方法的。它们是纯粹的、易读的。尽可能的使用它们。

propTypes
import React from "react"
import { observer } from "mobx-react"
import { func, bool } from "prop-types"

import "./styles/Form.css"

ExpandableForm.propTypes = {
  onSubmit: func.isRequired,
  expanded: bool
}
// Component declaration

在声明组件之前,给组件定义 propTypes,因为这样它们可以立即被看见。
我们可以这样做,因为 JavaScript 有函数提升(function hoisting)。

解构 Props 和 defaultProps
import React from "react"
import { observer } from "mobx-react"
import { func, bool } from "prop-types"

import "./styles/Form.css"

ExpandableForm.propTypes = {
  onSubmit: func.isRequired,
  expanded: bool,
  onExpand: func.isRequired
}

function ExpandableForm(props) {
  const formStyle = props.expanded ? {height: "auto"} : {height: 0}
  return (
    
{props.children}
) }

我们的组件是一个函数,函数的参数就是组件的 props。我们可以使用解构参数的方式:

import React from "react"
import { observer } from "mobx-react"

import { func, bool } from "prop-types"
import "./styles/Form.css"

ExpandableForm.propTypes = {
  onSubmit: func.isRequired,
  expanded: bool,
  onExpand: func.isRequired
}

function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
  const formStyle = expanded ? {height: "auto"} : {height: 0}
  return (
    
{children}
) }

注意,我们还可以使用默认参数作为 defaultProps,这种方式可读性更强。
如果 expanded 未定义,则将其设置为false。(这样可以避免类似 ‘Cannot read of undefined’ 之类的错误)

避免使用函数表达式的方式来定义组件,如下:

const ExpandableForm = ({ onExpand, expanded, children }) => {

这看起来非常酷,但是在这里,通过函数表达式定义的函数却是匿名函数。

如果 Bable 没有做相关的命名配置,那么报错时,错误堆栈中不会告诉具体是哪个组件出错了,只会显示 <> 。这使得调试变得非常糟糕。

匿名函数也可能会导致 React 测试库 Jest 出问题。由于这些潜在的隐患,我们推荐使用函数声明,而不是函数表达式。

包裹函数

因为基于函数的组件不能使用修饰器,所以你应当将基于函数的组件当做参数,传给修饰器对应的函数:

import React from "react"
import { observer } from "mobx-react"
import { func, bool } from "prop-types"

import "./styles/Form.css"

ExpandableForm.propTypes = {
  onSubmit: func.isRequired,
  expanded: bool,
  onExpand: func.isRequired
}

function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
  const formStyle = expanded ? {height: "auto"} : {height: 0}
  return (
    
{children}
) } export default observer(ExpandableForm)

全部的代码如下:

import React from "react"
import { observer } from "mobx-react"
import { func, bool } from "prop-types"
// Separate local imports from dependencies
import "./styles/Form.css"

// Declare propTypes here, before the component (taking advantage of JS function hoisting)
// You want these to be as visible as possible
ExpandableForm.propTypes = {
  onSubmit: func.isRequired,
  expanded: bool,
  onExpand: func.isRequired
}

// Destructure props like so, and use default arguments as a way of setting defaultProps
function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
  const formStyle = expanded ? { height: "auto" } : { height: 0 }
  return (
    
{children}
) } // Wrap the component instead of decorating it export default observer(ExpandableForm)
JSX 中的条件表达式

很可能你会做很多条件渲染。这是你想避免的:

不,三目嵌套不是一个好主意。

有一些库解决了这个问题(JSX-Control Statementments),但是为了引入另一个依赖库,我们使用复杂的条件表达式,解决了这个问题:

使用大括号包裹一个立即执行函数(IIFE),然后把你的 if 语句放在里面,返回你想要渲染的任何东西。
请注意,像这样的 IIFE 可能会导致一些性能消耗,但在大多数情况下,可读性更加重要。

更新:许多评论者建议将此逻辑提取到子组件,由这些子组件返回的不同 button。这是对的,尽可能地拆分组件。

另外,当你有布尔判断渲染元素时,不应该这样做:

{
  isTrue
   ? 

True!

: }

应该使用短路运算:

{
  isTrue && 
    

True!

}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/84884.html

相关文章

  • React 最佳实践

    摘要:本文针对技术栈,总结了一些最佳实践,对编写高质量的代码有一定的参考作用。二最佳实践说明多用如果组件是纯展示型的,不需要维护和生命周期,则优先使用。理解并遵循这些最佳实践,写出来的代码质量会有一定的保证。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 在日常开发和 Code Revi...

    lavnFan 评论0 收藏0
  • reactreact-router、redux 也许是最佳实践1

    摘要:通过声明式编程模型定义组件,是最强大的核心功能。无论是的浏览器书签,还是的导航功能,只要是可以使用的地方,就可以使用。二级路由使用渲染组件属性状态请选择一个主题。也许是最佳小实践地址,觉得有帮助的话,请点击一下,嘿嘿 小前言 这是一个小小的有关react的小例子,希望通过一个小例子,可以让新手更好的了解到react、react-router4.0、redux的集中使用方法。 这是基...

    Betta 评论0 收藏0
  • [译]React Component最佳实践

    摘要:引入初始化使用句法定义初始化和和的声明应该置顶便于其他开发者阅读。在版本,推荐使用这个包替代。组件内的方法使用在方法中箭头函数来替代是异步的。高阶组件完整代码在组件前声明解构通过函数入参默认值的方式设定 原文:Our Best Practices for Writing React Components . 这里意译。有些点在之前的文章里提到过:#2译文地址:https://githu...

    Alfred 评论0 收藏0
  • React.js 最佳实践(2016)_链接修正版

    摘要:译者按最近依旧如火如荼相信大家都跃跃欲试我们团队也开始在领域有所尝试年应该是逐渐走向成熟的一年让我们一起来看看国外的开发者们都总结了哪些最佳实践年在全世界都有很多关于新的更新和开发者大会的讨论关于去年的重要事件请参考那么年最有趣的问题来了我 译者按:最近React(web/native)依旧如火如荼,相信大家都跃跃欲试,我们团队也开始在React领域有所尝试. 2016年应该是Reac...

    syoya 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<