編寫React組件的最佳實踐

DarWhitt 7年前發布 | 18K 次閱讀 React

在我第一次編寫 React 代碼的時候,我見發現許多不同的方法可以用來編寫組件,不同教程教授的內容也大不相同。盡管從那時候起框架已經相當成熟,但并沒有一種固定的“正確”方式指導。

在  MuseFind  工作的一年里,我們的團隊編寫了許多 React 組件,后期我們對方法進行了優化直到滿意為止。

本指南描述了我們推薦的最佳實踐,不管你是一名初學者還是有經驗的老手,希望它能對你有所幫助。

在我們開始之前,有幾個地方要注意一下:

  • 我們使用的是 ES6 和 ES7 的語法。

  • 如果你對于現實和容器組件兩者之間的區別不甚明了,建議首先閱讀一下 這個

  • 如果有任何建議、疑問或者感想,請通過評論來讓我們知曉。

基于類的組件

基于類的組件具有豐富狀態而且可以含有方法。我們要盡可能有節制地去使用,因為它們也有特定的適用場合。

讓我們使用一行一行的代碼逐步地將我們的組件構建起來吧。

引入 CSS

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

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

我喜歡 在 JavaScript 中操作 CSS ,這在理論上這樣做是可行的。不過它仍然是一種新的創意,還沒出現切實可行的解決方案。不過在此之前,我們可以先為每一個組件引入一個 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 }

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 的箭頭函數可以自動地維護好正確的上線文。

屬性析構

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>     )   } }

擁有許多屬性的組件要讓每個屬性都另起一行,如上所示。

裝飾器

@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 組件,不管它的其它屬性實際是否已經發生了變化,都會自動地觸發讓它重新渲染。

調和是 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

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>
  )
}

我的組件是一個函數,因此可以將它的屬性看做是參數。我們可以像下面這樣對它們進行擴展:

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 的角色。如果 expanded 是 undefined, 我們就會將其設置為 false。 (這個例子有點勉強,因為是一個布爾值,不過本身對于避免對象的“Cannot read <property> of undefined“這樣的錯誤是很有用的)。

要避免如下這種 ES6 語法:

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

看著非常現代,不過這里的函數實際上沒有被命令。

這樣子的名稱在 Bable 進行了正確的設置的情況下是可行的 — 但如果沒有正確設置,任何錯誤都會以在<<anonymous>>中出現的方式顯示,調試起來相當麻煩。

無名的函數也會在 Jest 這個 React 測試庫中引發問題。為了避免潛在的復雜問題出現,我們建議使用 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)

如下是完整的組件代碼:

import React from 'react'
import {observer} from 'mobx-react'
// Separate local imports from dependencies
import './styles/Form.css'

// Declare propTypes here as a variable, then assign below function declaration 
// You want these to be as visible as possible
const expandableFormRequiredProps = {
  onSubmit: React.PropTypes.func.isRequired,
  expanded: React.PropTypes.bool
}

// Destructure props like so, and use default arguments as a way of setting defaultProps
function ExpandableForm({ onExpand, expanded = false, children }) {
  return (
    <form style={ expanded ? { height: 'auto' } : { height: 0 } }>       {children}       <button onClick={onExpand}>Expand</button>     </form>
  )
}

// Set propTypes down here to those declared above
ExpandableForm.propTypes = expandableFormRequiredProps

// Wrap the component instead of decorating it
export default observer(ExpandableForm)

JSX 中的條件分支

你會有不少機會去做許多條件分支渲染。如下是你想要去避免的情況:

嵌套的三元組并非不是好主意。

有一些庫可以解決這個問題 ( JSX-Control Statements ),不過相比引入額外的依賴,通過如下這種方式解決復雜條件分支問題要更好:

使用花括弧封裝一個 IIFE , 然后在里面放入 if 語句,可以返回任何你想要渲染的東西。注意像這樣的 IIFE 對性能會有影響,不過在大多數情況中還不足以讓我們為此選擇丟掉可讀性。

還有就是當你只想要在一個條件分支中渲染一個元素時,比起這樣做…

{
  isTrue
   ? <p>True!</p>
   : <none/>
}

… 使用短路寫法更劃算:

{
  isTrue && 
    <p>True!</p>
}

 

來自:http://developer.51cto.com/art/201702/533215.htm

 

 本文由用戶 DarWhitt 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!