本文爲譯文,已得到原做者容許,原文地址:http://scottdomes.com/blog/ou...css
當我第一次開始寫 React 時,我發現多少個 React 教程,就有多少種寫 React 組件方法。雖然現在,框架已經成熟,可是並無一個 「正確」 寫組件的方法。node
在 MuseFind 的一年以來,咱們的團隊寫了大量的 React 組件。咱們精益求精,不斷完善寫 React 組件的方法。react
本文介紹了,咱們團隊寫 React 組件的最佳實踐。
咱們但願,不管你是初學者,仍是經驗豐富的人,這篇文章都會對你有用的。git
在開始介紹以前,先說幾個點:es6
咱們團隊使用 ES6 和 ES7 的語法。github
若是不清楚表現組件(presentational components)和容器組件(container components)之間的區別,咱們建議先閱讀 這篇文章。閉包
若是有任何建議,問題或反饋意見,請在評論中通知咱們。框架
基於類的組件(Class based components)是包含狀態和方法的。
咱們應該儘量地使用基於函數的組件(Functional Components
)來代替它們。可是,如今讓咱們先來說講怎麼寫基於類的組件。dom
讓咱們逐行地構建咱們的組件。異步
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 這種簡單的初始化狀態的新方法。
更多,請閱讀 這裏。
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' }
propTypes
和 defaultProps
是靜態屬性(static properties),在組件代碼中,最好把它們寫在組件靠前的位置。當其餘開發人員查看這個組件的代碼時,應該當即看到 propTypes
和 defaultProps
,由於它們就好像這個組件的文檔同樣。(譯註:關於組件書寫的順序,參考 這篇文章)
若是使用 React 15.3.0 或更高版本,請使用 prop-types 代替 React.PropTypes。使用 prop-types
時,應當將其解構。
全部組件都應該有 propTypes
。
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 }))
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 ( <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
值時,每一個 prop
應當單獨佔據一行。
@observer export default class ProfileContainer extends Component {
若是使用 mobx
,那麼應當是用裝飾器(decorators)。其本質是將裝飾器的組件傳遞到一個函數。
使用裝飾器一種更加靈活和更加可讀的方式。
咱們團隊在使用 mobx
和咱們本身的 mobx-models
庫時,使用了大量的裝飾器。
若是您不想使用裝飾器,也能夠按照下面的方式作:
class ProfileContainer extends Component { // Component code } export default observer(ProfileContainer)
避免傳遞一個新閉包(Closures)給子組件,像下面這樣:
<input type="text" value={model.name} // onChange={(e) => { 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 ( <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> ) } }
基於函數的組件(Functional Components)是沒有狀態和方法的。它們是純粹的、易讀的。儘量的使用它們。
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)。
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 ( <form style={formStyle} onSubmit={props.onSubmit}> {props.children} <button onClick={props.onExpand}>Expand</button> </form> ) }
咱們的組件是一個函數,函數的參數就是組件的 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 ( <form style={formStyle} onSubmit={onSubmit}> {children} <button onClick={onExpand}>Expand</button> </form> ) }
注意,咱們還可使用默認參數做爲 defaultProps
,這種方式可讀性更強。
若是 expanded
未定義,則將其設置爲false。(這樣能夠避免相似 ‘Cannot read <property> of undefined’ 之類的錯誤)
避免使用函數表達式的方式來定義組件,以下:
const ExpandableForm = ({ onExpand, expanded, children }) => {
這看起來很是酷,可是在這裏,經過函數表達式定義的函數倒是匿名函數。
若是 Bable 沒有作相關的命名配置,那麼報錯時,錯誤堆棧中不會告訴具體是哪一個組件出錯了,只會顯示 <<anonymous>> 。這使得調試變得很是糟糕。
匿名函數也可能會致使 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 ( <form style={formStyle} onSubmit={onSubmit}> {children} <button onClick={onExpand}>Expand</button> </form> ) } 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 ( <form style={formStyle} onSubmit={onSubmit}> {children} <button onClick={onExpand}>Expand</button> </form> ) } // Wrap the component instead of decorating it export default observer(ExpandableForm)
極可能你會作不少條件渲染。這是你想避免的:
不,三目嵌套不是一個好主意。
有一些庫解決了這個問題(JSX-Control Statementments),可是爲了引入另外一個依賴庫,咱們使用複雜的條件表達式,解決了這個問題:
使用大括號包裹一個當即執行函數(IIFE),而後把你的 if
語句放在裏面,返回你想要渲染的任何東西。
請注意,像這樣的 IIFE 可能會致使一些性能消耗,但在大多數狀況下,可讀性更加劇要。
更新:許多評論者建議將此邏輯提取到子組件,由這些子組件返回的不一樣 button
。這是對的,儘量地拆分組件。
另外,當你有布爾判斷渲染元素時,不該該這樣作:
{ isTrue ? <p>True!</p> : <none/> }
應該使用短路運算:
{ isTrue && <p>True!</p> }