React 最佳實踐
歡迎關注我的公眾號 睿Talk
,獲取我最新的文章:
一、前言
在日常開發和 Code Review 的時候,常常會發現一些共性的問題,也有很多值得提倡的做法。本文針對 React 技術棧,總結了一些最佳實踐,對編寫高質量的程式碼有一定的參考作用。
二、最佳實踐 & 說明
- 多用 Function Component
如果元件是純展示型的,不需要維護 state 和生命週期,則優先使用 Function Component。它有如下好處:
- 程式碼更簡潔,一看就知道是純展示型的,沒有複雜的業務邏輯
- 更好的複用性。只要傳入相同結構的 props,就能展示相同的介面,不需要考慮副作用。
- 更小的打包體積,更高的執行效率
- 多用 PureComponent
如果元件需要維護 state 或使用生命週期方法,則優先使用 PureComponent,而不是 Component。Component 的預設行為是不論 state 和 props 是否有變化,都觸發 render。而 PureComponent 會先對 state 和 props 進行淺比較,不同的時候才會 render。請看下面的例子:
class Child extends React.Component { render() { console.log('render Child'); return ( <div> {this.props.obj.num} </div> ); } } class App extends React.Component { state = { obj: { num: 1 } }; onClick = () => { const {obj} = this.state; this.setState({obj}); } render() { console.log('render Parent'); return ( <div className="App" > <button onClick={this.onClick}> 點我 </button> <Child obj={this.state.obj}/> </div> ); } }
點選按鈕後,Parent 和 Child 的 render 都會觸發。如果將 Child 改為 PureComponent,則 Child 的 render 不會觸發,因為 props 還是同一個物件。如果將 Parent 也改為 PureComponent,則 Parent 的 render 也不會觸發了,因為 state 還是同一個物件。
- 定義元件時,定義 PropTypes 和 defaultProps
例子如下:
class CategorySelector extends PureComponent { ... } CategorySelector.propTypes = { type: PropTypes.string, catList: PropTypes.array.isRequired, default: PropTypes.bool, }; CategorySelector.defaultProps = { default: false, type: undefined, };
- 避免在 render 裡面動態建立物件 / 方法,否則會導致子元件每次都 render
render() { const obj = {num: 1} return( <Child obj={obj} onClick={()=>{...}} /> ); }
在上面程式碼中,即使 Child 是 PureComponent,由於 obj 和 onClick 每次 render 都是新的物件,Child 也會跟著 render。
- 避免在 JSX 中寫複雜的三元表示式,應通過封裝函式或元件實現
render() { const a = 8; return ( <div> { a > 0 ? a < 9 ? ... : ... : ... } </div> ); }
避免寫像上面這種巢狀的三元表示式。可以寫成下面的形式:
f() { ... } render() { const a = 8; return ( <div> { this.f() } </div> ); }
- 多使用解構,如 Function Component 的 props
const MenuItem = ({ menuId, menuText, onClick, activeId, }) => { function onItemClick() { onClick(menuId); } return ( <div menuId={menuId} className={`${style} ${activeId === menuId ? active : ''}`} onClick={onItemClick} > {menuText} </div> ); };
- 如果 props 的資料不會改變,就不需要在 state 或者元件例項屬性裡拷貝一份
經常會看見這樣的程式碼:
componentWillReceiveProps(nextProps) { this.setState({num: nextProps.num}); } render() { return( <div>{this.state.num}</div> ); }
num 在元件中不會做任何的改變,這種情況下直接使用 this.props.num 就可以了。
- 遵循單一職責原則,使用 HOC / 裝飾器 / Render Props 增加職責
比如一個公用的元件,資料來源可能是父元件傳過來,又或者是自己主動通過網路請求獲取資料。這時候可以先定義一個純展示型的 Function Component,然後再定義一個高階元件去獲取資料:
function Comp() { ... } class HOC extends PureComponent { async componentDidMount() { const data = fetchData(); this.setState({data}); } render() { return (<Comp data={this.state.data}/>); } }
- 組合優於繼承
筆者在真實專案中就用過 2 次以繼承的形式寫元件,自己寫得很爽,程式碼的複用性也很好,但最大的問題是別人看不懂。我將複用的業務邏輯和 UI 模版都在父類定義好,子類只需要傳入一些引數,然後再覆蓋父類的幾個方法就好(render的時候會用到)。簡化的程式碼如下:
class Parent extends PureComponent { componentDidMount() { this.fetchData(this.url); } fetchData(url) { ... } render() { const data = this.calcData(); return ( <div>{data}</data> ); } } class Child extends Parent { constructor(props) { super(props); this.url = 'http://api'; } calcData() { ... } }
這樣的寫法從語言的特性和功能實現來說,沒有任何問題,最大的問題是不符合 React 的元件編寫習慣。父類或者子類肯定有一方是不需要實現 render 方法的,而一般我們看程式碼都會優先找 render 方法,找不到就慌了。另外就是搞不清楚哪些方法是父類實現的,哪些方法是子類實現的,如果讓其他人來維護這份程式碼,會比較吃力。
繼承會讓程式碼難以溯源,定位問題也比較麻煩。所有通過繼承實現的元件都可以改寫為組合的形式。上面的程式碼就可以這樣改寫:
class Parent extends PureComponent { componentDidMount() { this.fetchData(this.props.url); } fetchData(url) { ... } render() { const data = this.props.calcData(this.state); return ( <div>{data}</data> ); } } class Child extends PureComponent { calcData(state) { ... } render() { <Parent url="http://api" calcData={this.calcData}/> } }
這樣的程式碼是不是看起來舒服多了?
三、總結
本文列舉了筆者在專案實戰和 Code Review 過程中總結的一些最佳實踐,只代表個人立場。理解並遵循這些最佳實踐,寫出來的程式碼質量都有一定的保證。如果你有不同的意見,或者有補充的最佳實踐,歡迎留言。