React 最佳實踐

歡迎關注我的公衆號睿Talk,獲取我最新的文章:
clipboard.png

一、前言

在日常開發和 Code Review 的時候,常常會發現一些共性的問題,也有很多值得提倡的做法。本文針對 React 技術棧,總結了一些最佳實踐,對編寫高質量的代碼有一定的參考作用。

二、最佳實踐 & 說明

  • 多用 Function Component

如果組件是純展示型的,不需要維護 state 和生命週期,則優先使用 Function Component。它有如下好處:

  1. 代碼更簡潔,一看就知道是純展示型的,沒有複雜的業務邏輯
  2. 更好的複用性。只要傳入相同結構的 props,就能展示相同的界面,不需要考慮副作用。
  3. 更小的打包體積,更高的執行效率
  • 多用 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 過程中總結的一些最佳實踐,只代表個人立場。理解並遵循這些最佳實踐,寫出來的代碼質量都有一定的保證。如果你有不同的意見,或者有補充的最佳實踐,歡迎留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章