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

Colafornia發表於2019-03-04

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

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

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

此言不虛!

讓我們開始吧!

1. 元件中的 bind() 與箭頭函式

在使用自定義函式作為元件屬性之前你必須將你的自定義函式寫在 constructor 中。如果你是用 extends 關鍵字宣告元件的話,自定義函式(如下面的 updateValue 函式)會失去 this 繫結。因此,如果你想使用 this.statethis.props 或者 this.setState,你還得重新繫結。

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 中繫結。另一種方法是在傳值的時候作為屬性的值進行繫結:

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

這種方法有一個問題。由於 .bind() 每次執行時都會建立一個函式這種方法會導致每次 render **函式執行時都會建立一個新函式。**這會對效能造成一些影響。然而,在小型應用中這可能並不會造成顯著影響。隨著應用體積變大,差別就會開始顯現。這裡 有一個案例研究。

箭頭函式所涉及的效能問題與 bind 相同。

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

這種寫法明顯更清晰。可以看到 prop onChange 函式中發生了什麼。但是,這也導致了每次 input 元件渲染時都會建立一個新的匿名函式。因此,箭頭函式有同樣的效能弊端。

解決方案

避免上述效能弊端的最佳方法是在函式本身的構造器中進行繫結。這樣,在元件建立時僅建立了一個額外函式,即使再次執行 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 就會丟擲警告。

[譯] How to NOT React: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 將會報錯。

[譯] How to NOT React: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.

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

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


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

相關文章