標題本來想下 React anti-pattern ,但是我自己不是很喜歡所謂的 anti-pattern 或者是 best practice 這樣的名詞,因為一旦用上了這樣的名詞就會有一種只要照著這樣做就好,不會想去深究到底前提是怎樣,跟你想要解決什麼問題。因此最後我把標題命名成一些自己寫 React 的好習慣
不要在 componentDidMount componentDidUpdate 同步的調用 setState 或 dispatch 一個會同步觸發 reducer 的 action
也就是說盡量不要出現下面這種寫法:
componentDidMount() {
this.setState()
this.props.setReduxStateByAction()
}
如果在 lifcycle 結束的時候同步額外 setState 意味著 React 會根據新的 state 又重新開始第二次 render 的流程,最後瀏覽器在繪製的時候只會根據最後的 DOM 的結果,使得第一次的 render 所造成 DOM 的改變只是一個沒有意義冗餘浪費效能的過程,還記得 React 設計 virialDOM 的目的就是因為渲染真實的 DOM 是很昂貴的吧,而為什麼也不要 dispatch 會同步的觸發 reducer 的 action 呢,因為 react-redux 裡面 reducer 跑完接著就有機會去跑每一個 connect HOC 的 setState。
請注意我這邊用了同步這個詞,在 componentDidMount componentDidUpdate 裡 非同步 的調用 setState 是可以的,而像是 setTimeout, fetch 等非同步的方法,會讓瀏覽器可以把第一次渲染畫的出來,等非同步的事件觸發以後再重新渲染。另外 dispatch 會被處理 side effect 的 middleware 例如 thunk 攔截下來 action 也是可以的,不會觸發 setState
// it's OK
componentDidMount() {
setTimeout(() => this.setState(), 10)
fetch('url').then(() => this.setState())
this.props.thunkAction()
}
理論上應該先確定資料源 state 是什麼,在根據 state 把整個 Component 渲染出來。
所以你如果要做這些事情的時候,重新思考你是不是可以在 reducer 或是 Component 內部就先決定好 state 的初始值就好
盡量不要在 componentWillReceiveProps 調用 setState 跟用 props 初始化 state
componentWillReceiveProps(cWRP) 算是個特別的例子,在 cWRP setState 並不會又開始跑新的 rerender 的流程,而是在 update 這個 Component 前會先更新 state ,然後才會用新的 state 跟 props 去渲染這個 Component。所以不會有不必要的 rerender 效能問題。
parent render => cWRP => setState => component render with newState
然而 cWRP 本身要寫的漂亮跟正確其實不容易,因為通常只有特定的 prop 的變動才要更新 state,有的時候常常會漏了考慮某些狀況而出現 bug,cWRP 也不是一個完全 props 跟 state 兩者的 mapping 關係,只有在父層重新渲染的時候才會觸發,自己 setState 跟 Component 初始化是都不會觸發 cWRP
new props => cWRP => new State — only happens in parent update life cycle
如果從資料流的角度來看,render 的流程中才決定 state 是什麼,要維護正確結果,原本只需要管理每個 state 的 render 邏輯是否正確,瞬間變成要管理 state props 連動的邏輯*render 的邏輯二維的複雜度。
用 props 初始化 state 也是一個只存在在 mount life cycle mapping 關係,你用了這樣的寫法,就會讓 reviewer 要確認你是不是真的只需要初始化就好,update 的時候有沒有需要維持這 props 跟 state 的 mapping 關係,增加不少的確認跟溝通成本,大部分你需要這樣做的時候其實只需要用到 props 而不需要把 props 在丟到 state 裡面增加管理的複雜度。
initial props => constructor=> initial State — only happens in mount life cycle
在 React v16.3 cWRP 這個 API 即將被 getDrivedStateFromProps 取代,這個 function 會同時作用 Component 初始化跟 new props 傳遞下來的時候,在 mount 跟 update 都會執行,用 return 的方式取代手動 setState,讓 mapping的關係更容易被注意到以外,也比較不會漏掉狀況的處理,但不代表你應該使用他,Dan 在 twitter 上表示名字取的這麼長是故意的,因為不鼓勵大量使用這個方法。
如果我們把 view 寫成下面的的函式
View = f(state)
在理解 View 是怎麼變化的時候,能夠先決定並先控制好 state,render 過程中也不會改變 state,剩下的事情就輕鬆簡單了
因此如果遇到你想要在 componentWillReceiveProps setState 或是新版 getDrivedStateFromProps 的時候,你也許可以考慮下面兩個替代方案:
- Lift State Up 把 state 往父層 Component 集中,變成 props 傳下來
- 因為讓 state 跟 props 保持 mapping 的對應關係做不太到,所以應該盡量讓 state 跟 props 彼此獨立互相不影響,在調用時才用 state 跟 props 來決定行為
結語
其實我還蠻推薦可以在把 react 官方各個 lifecycle method 的用法再看一次,哪些是不好的用法 anti pattern 其實都寫的相當清楚。
https://reactjs.org/docs/react-component.html
最後 香港加油
繁體技術文章不多,或多或少有一些香港人會看到這篇文章,經歷過太陽花學運大概也可以感受到那種絕望跟憤恨,希望這篇文章不只幫你們解決技術上的問題,也能感受到台灣這邊很多人也是支持你們的,加油~~
2019/08/22