在我第一次編寫 React 程式碼的時候,我見發現許多不同的方法可以用來編寫元件,不同教程教授的內容也大不相同。儘管從那時候起框架已經相當成熟,但並沒有一種固定的“正確”方式指導。
在 MuseFind 工作的一年裡,我們的團隊編寫了許多 React 元件,後期我們對方法進行了優化直到滿意為止。
本指南描述了我們推薦的最佳實踐,不管你是一名初學者還是有經驗的老手,希望它能對你有所幫助。
在我們開始之前,有幾個地方要注意一下:
- 我們使用的是 ES6 和 ES7 的語法。
- 如果你對於現實和容器元件兩者之間的區別不甚明瞭,建議首先閱讀一下這個。
- 如果有任何建議、疑問或者感想,請通過評論來讓我們知曉。
基於類的元件
基於類的元件具有豐富狀態而且可以含有方法。我們要儘可能有節制地去使用,因為它們也有特定的適用場合。
讓我們使用一行一行的程式碼逐步地將我們的元件構建起來吧。
引入 CSS
1 2 3 4 5 |
import React, {Component} from 'react' import {observer} from 'mobx-react' import ExpandableForm from './ExpandableForm' import './styles/ProfileContainer.css' |
我喜歡在 JavaScript 中操作 CSS,這在理論上這樣做是可行的。不過它仍然是一種新的創意,還沒出現切實可行的解決方案。不過在此之前,我們可以先為每一個元件引入一個 CSS 檔案。
我們也通過另寫一行將依賴引入從本地引入獨立了出來。
狀態初始化
1 2 3 4 5 6 7 8 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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。
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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 的箭頭函式可以自動地維護好正確的上線文。
屬性析構
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
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> ) } } |
擁有許多屬性的元件要讓每個屬性都另起一行,如上所示。
裝飾器
1 2 |
@observer export default class ProfileContainer extends Component { |
如果你使用了一些像 mobx 的東西,就可以像上面這樣對類元件進行裝飾 — 這樣做跟將元件傳遞給一個函式是一樣的效果。
裝飾器是一種用來修改元件功能的靈活且可讀性好的方式。我們對其進行了廣泛的運用,包括 mobx 還有我們自己的 mobx-models 庫。
如果你不想使用裝飾器,可以這樣做:
1 2 3 4 |
class ProfileContainer extends Component { // Component code } export default observer(ProfileContainer) |
閉包
要避免向子元件傳遞新的閉包,如下:
1 2 3 4 5 6 7 |
<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 中消耗最昂貴的部分,因此不要讓它的計算難度超過所需! 另外,傳遞一個類方法更容易閱讀、除錯和修改。
如下是完整的元件程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
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
1 2 3 4 5 6 7 8 9 |
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 進行析構
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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> ) } |
我的元件是一個函式,因此可以將它的屬性看做是引數。我們可以像下面這樣對它們進行擴充套件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 語法:
1 |
const ExpandableForm = ({ onExpand, expanded, children }) => { |
看著非常現代,不過這裡的函式實際上沒有被命令。
這樣子的名稱在 Bable 進行了正確的設定的情況下是可行的 — 但如果沒有正確設定,任何錯誤都會以在<<anonymous>>中出現的方式顯示,除錯起來相當麻煩。
無名的函式也會在 Jest 這個 React 測試庫中引發問題。為了避免潛在的複雜問題出現,我們建議使用 function 而不是 const。
封裝
因為在函式式元件中不能使用裝飾器,所以你可以簡單地將它傳遞到函式中充當引數:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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) |
如下是完整的元件程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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 對效能會有影響,不過在大多數情況中還不足以讓我們為此選擇丟掉可讀性。
還有就是當你只想要在一個條件分支中渲染一個元素時,比起這樣做…
1 2 3 4 5 |
{ isTrue ? <p>True!</p> : <none/> } |
… 使用短路寫法更划算:
1 2 3 4 |
{ isTrue && <p>True!</p> } |