- 原文地址:How to NOT React: Common Anti-Patterns and Gotchas in React
- 原文作者:NeONBRAND
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:MechanicianW
- 校對者:anxsec ClarenceC
什麼是反模式?反模式是軟體開發中被認為是糟糕的程式設計實踐的特定模式。同樣的模式,可能在過去一度被認為是正確的,但是現在開發者們已經發現,從長遠來看,它們會造成更多的痛苦和難以追蹤的 Bug。
作為一個 UI 庫,React 已經成熟,並且隨著時間的推移,許多最佳實踐也逐漸形成。我們將從數千名開發者集體的智慧中學習,他們曾用笨方法(the hard way)學習這些最佳實踐。
此言不虛!
讓我們開始吧!
1. 元件中的 bind() 與箭頭函式
在使用自定義函式作為元件屬性之前你必須將你的自定義函式寫在 constructor
中。如果你是用 extends
關鍵字宣告元件的話,自定義函式(如下面的 updateValue
函式)會失去 this
繫結。因此,如果你想使用 this.state
,this.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>
)
}
}
複製程式碼
延伸閱讀
- React 繫結模式: 5 個處理
this
的方法 - React.js pure render 效能反模式
- React —— 繫結還是不繫結
- 在 React component classes 中繫結函式的原因及方法
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 元件主要由三部分組成:state
,props
和標記(或其它元件)。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 可能不會重新渲染。這是因為 state
和 props
是非同步更新的。也就是說,DOM 並不會隨著 setState
被呼叫就立即更新。React 會將多次更新合併到同一批次進行更新,然後渲染 DOM。查詢 state
物件時,你可能會收到已經過期的值。文件也提到了這一點:
由於
this.props
和this.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
傳入一個接收 currentState 和 currentProps 作為引數的函式。這個函式的返回值會與當前 state 合併以形成新的 state。
延伸閱讀
- Dan Abramov 對於為什麼
setState
是非同步的所做的超級棒的解釋 - 在
setState
中使用函式而不是物件 - Beware: React 的 setState 是非同步的!
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 全棧開發。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。