[譯] How to NOT React:React 中常見的反模式與陷阱

什麼是反模式?反模式是軟件開發中被認爲是糟糕的編程實踐的特定模式。一樣的模式,可能在過去一度被認爲是正確的,可是如今開發者們已經發現,從長遠來看,它們會形成更多的痛苦和難以追蹤的 Bug。html

做爲一個 UI 庫,React 已經成熟,而且隨着時間的推移,許多最佳實踐也逐漸造成。咱們將從數千名開發者集體的智慧中學習,他們曾用笨方法(the hard way)學習這些最佳實踐。前端

此言不虛!react

讓咱們開始吧!android

1. 組件中的 bind() 與箭頭函數

在使用自定義函數做爲組件屬性以前你必須將你的自定義函數寫在 constructor 中。若是你是用 extends 關鍵字聲明組件的話,自定義函數(以下面的 updateValue 函數)會失去 this 綁定。所以,若是你想使用 this.statethis.props 或者 this.setState,你還得從新綁定。webpack

Demo

class app extends Component {
  constructor(props) {
    super(props);
    this.state = {
      name: ''
    };
    this.updateValue = this.updateValue.bind(this);
  }

updateValue(evt) {
    this.setState({
      name: evt.target.value
    });
  }

render() {
    return (
      <form>
        <input onChange={this.updateValue} value={this.state.name} />
      </form>
    )
  }
}
複製代碼

問題

有兩種方法能夠將自定義函數綁定到組件的 this。一種方法是如上面所作的那樣,在 constructor 中綁定。另外一種方法是在傳值的時候做爲屬性的值進行綁定:ios

<input onChange={this.updateValue.bind(this)} value={this.state.name} />
複製代碼

這種方法有一個問題。因爲 .bind() 每次運行時都會建立一個函數這種方法會致使每次 render **函數執行時都會建立一個新函數。**這會對性能形成一些影響。然而,在小型應用中這可能並不會形成顯著影響。隨着應用體積變大,差異就會開始顯現。這裏 有一個案例研究。git

箭頭函數所涉及的性能問題與 bind 相同。github

<input onChange={ (evt) => this.setState({ name: evt.target.value }) } value={this.state.name} />
複製代碼

這種寫法明顯更清晰。能夠看到 prop onChange 函數中發生了什麼。可是,這也致使了每次 input 組件渲染時都會建立一個新的匿名函數。所以,箭頭函數有一樣的性能弊端。web

解決方案

避免上述性能弊端的最佳方法是在函數自己的構造器中進行綁定。這樣,在組件建立時僅建立了一個額外函數,即便再次執行 render 也會使用該函數。數據庫

有一種狀況常常發生就是你忘記在構造函數中去 bind 你的函數,而後就會收到報錯(Cannot find X on undefined.)。Babel 有個插件可讓咱們使用箭頭語法寫出自動綁定的函數。插件是 Class properties transform。如今你能夠這樣編寫組件:

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      name: ''
    };

// 看!無需在此處進行函數綁定!

}
updateValue = (evt) => {
    this.setState({
      name: evt.target.value
    });
  }

render() {
    return (
      <form>
        <input onChange={this.updateValue} value={this.state.name} />
      </form>
    )
  }
}
複製代碼

延伸閱讀

2. 在 key prop 中使用索引

遍歷元素集合時,key 是必不可少的 prop。key 應該是穩定,惟一,可預測的,這樣 React 才能追蹤元素。key 是用來幫助 React 輕鬆調和虛擬 DOM 與真實 DOM 間的差別的。然而,使用某些值集例如數組索引可能會致使你的應用崩潰或是渲染出錯誤數據。

Demo

{elements.map((element, index) =>
    <Display
       {...element}
       key={index}
       />
   )
}
複製代碼

問題

當子元素有了 key,React 就會使用 key 來匹配原始樹結構和後續樹結構中的子元素。**key 被用於做身份標識。**若是兩個元素有一樣的 key,React 就會認爲它們是相同的。當 key 衝突了,即超過兩個元素具備一樣的 key,React 就會拋出警告。

警告出現重複的 key。

這裏 是 CodePen 上使用索引做爲 key 可能致使的問題的一個示例。

解決方案

被使用的 key 應該是:

  • 惟一的: 元素的 key 在它的兄弟元素中應該是惟一的。沒有必要擁有全局惟一的 key。
  • 穩定的: 元素的 key 不該隨着時間,頁面刷新或是元素從新排序而變。
  • 可預測的: 你能夠在須要時拿到一樣的 key,意思是 key 不該是隨機生成的。

數組索引是惟一且可預測的。然而,並不穩定。一樣,隨機數或時間戳不該被用做爲 key。

因爲隨機數既不惟一也不穩定,使用隨機數就至關於根本沒有使用 key。即便內容沒有改變,組件也每次都從新渲染。

時間戳既不穩定也不可預測。**時間戳也會一直遞增。**所以每次刷新頁面,你都會獲得新的時間戳。

一般,你應該依賴於數據庫生成的 ID 如關係數據庫的主鍵,Mongo 中的對象 ID。若是數據庫 ID 不可用,你能夠生成內容的哈希值來做爲 key。關於哈希值的更多內容能夠在這裏閱讀。

延伸閱讀

3. setState() 是異步的

React 組件主要由三部分組成:stateprops 和標記(或其它組件)。props 是不可變的,state 是可變的。state 的改變會致使組件從新渲染。若是 state 是由組件在內部管理的,則使用 this.setState 來更新 state。關於這個函數有幾件重要的事須要注意。咱們來看看:

Demo

class MyComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 350
    };
  }

  updateCounter() {
    // 這行代碼不會生效
    this.state.counter = this.state.counter + this.props.increment;

    // ---------------------------------

    // 不會如預期生效
    this.setState({
      counter: this.state.counter + this.props.increment; // 可能不會渲染
    });

    this.setState({
      counter: this.state.counter + this.props.increment; // this.state.counter 的值是什麼?
    });

    // ---------------------------------

    // 如期生效
    this.setState((prevState, props) => ({
      counter: prevState.counter + props.increment
    }));

    this.setState((prevState, props) => ({
      counter: prevState.counter + props.increment
    }));
  }
}
複製代碼

問題

請注意第 11 行代碼。若是你直接修改了 state,組件並不會從新渲染,修改也不會有任何體現。這是由於 state 是進行淺比較(shallow compare)的。你應該永遠都使用 setState 來改變 state 的值。

如今,若是你在 setState 中經過當前的 state 值來更新至下一個 state (正如第 15 行代碼所作的),React 可能不會從新渲染。這是由於 stateprops 是異步更新的。也就是說,DOM 並不會隨着 setState 被調用就當即更新。React 會將屢次更新合併到同一批次進行更新,而後渲染 DOM。查詢 state 對象時,你可能會收到已通過期的值。文檔也提到了這一點:

因爲 this.propsthis.state 是異步更新的,你不該該依賴它們的值來計算下一個 state。

另外一個問題出現於一個函數中有屢次 setState 調用時,如第 16 和 20 行代碼所示。counter 的初始值是 350。假設 this.props.increment 的值是 10。你可能覺得在第 16 行代碼第一次調用 setState 後,counter 的值會變成 350+10 = **360。**而且,當第 20 行代碼再次調用 setState 時,counter 的值會變成 360+10 = 370。然而,這並不會發生。第二次調用時所看到的 counter 的值仍爲 350。**這是由於 setState 是異步的。**counter 的值直到下一個更新週期前都不會發生改變。setState 的執行在事件循環中等待,直到 updateCounter 執行完畢前,setState 都不會執行, 所以 state 的值也不會更新。

解決方案

你應該看看第 27 和 31 行代碼使用 setState 的方式。以這種方式,你能夠給 setState 傳入一個接收 currentStatecurrentProps 做爲參數的函數。這個函數的返回值會與當前 state 合併以造成新的 state。

延伸閱讀

4. 初始值中的 props

React 文檔提到這也是反模式:

在 getInitialState 中使用 props 來生成 state 常常會致使重複的「事實來源」,即真實數據的所在位置。這是由於 getInitialState 僅僅在組件第一次建立時被調用。

Demo

import React, { Component } from 'react'

class MyComponent extends Component {
  constructor(props){
    super(props);
    this.state = {
      someValue: props.someValue,
    };
  }
}
複製代碼

問題

constructor(getInitialState) 僅僅在組件建立階段被調用。也就是說,constructor 只被調用一次。所以,當你下一次改變 props 時,state 並不會更新,它仍然保持爲以前的值。

經驗尚淺的開發者常常設想 props 的值與 state 是同步的,隨着 props 改變,state 也會隨之變化。然而,真實狀況並非這樣。

解決方案

若是你須要特定的行爲即你但願 state 僅由 props 的值生成一次的話,可使用這種模式。state 將由組件在內部管理。

在另外一個場景下,你能夠經過生命週期方法 componentWillReceiveProps 保持 state 與 props 的同步,以下所示。

import React, { Component } from 'react'

class MyComponent extends Component {
  constructor(props){
    super(props);
    this.state = {
      someValue: props.someValue,
    };
  }

  componentWillReceiveProps(nextProps){
    if (nextProps.inputValue !== this.props.inputValue) {
      this.setState({ inputVal: nextProps.inputValue })
    }
  }
}
複製代碼

要注意,關於使用 componentWillReceiveProps 有一些注意事項。你能夠在文檔中閱讀。

最佳方法是使用狀態管理庫如 Redux 去 connect state 和組件。

延伸閱讀

5. 組件命名

在 React 中,若是你想使用 JSX 渲染你的組件,組件名必須以大寫字母開頭。

Demo

<MyComponent>
    <app /> // 不會生效 :(
</MyComponent>

<MyComponent>
    <App /> // 能夠生效!
</MyComponent>
複製代碼

問題

若是你建立了一個 app 組件,以 <app label="Save" /> 的形式去渲染它,React 將會報錯。

使用非大寫自定義組件時的警告。

報錯代表 <app> 是沒法識別的。只有 HTML 元素和 SVG 標籤能夠以小寫字母開頭。所以 <div /> 是能夠識別的,<app> 卻不能。

解決方案

你須要確保在 JSX 中使用的自定義組件是以大寫字母開頭的。

可是也要明白,聲明組件無需聽從這一規則。所以,你能夠這樣寫:

// 在這裏以小寫字母開頭是能夠的
class primaryButton extends Component {
  render() {
    return <div />;
  }
}

export default primaryButton;

// 在另外一個文件中引入這個按鈕組件。要確保以大寫字母開頭的名字引入。

import PrimaryButton from 'primaryButton';

<PrimaryButton />
複製代碼

延伸閱讀

以上這些都是 React 中不直觀,難以理解也容易出現問題的地方。若是你知道任何其它的反模式,請回複本文。😀


我還寫了一篇 能夠幫助快速開發的優秀 React 和 Redux 包

若是你仍在學習如何構建 React 項目,這個含有兩部分的系列文章 能夠幫助你理解 React 構建系統的多個方面。


我寫做 JavaScript,Web 開發與計算機科學領域的文章。關注我能夠每週閱讀新文章。若是你喜歡,能夠分享本文。

關注我 @ Facebook @ Linkedin @ Twitter.

✉️ 訂閱 CodeBurst的每週郵件 Email Blast, 🐦能夠在Twitter 上關注 CodeBurst, 瀏覽 🗺️ The 2018 Web Developer Roadmap, 和 🕸️ 學習 Web 全棧開發


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索