再次突破眼界。
簡述
在我們要跨組件溝通時,很常會愈到「props drilling」的問題,所以為了解決這個問題就有了 Redux 或 Context 的出現。
最近正好看了一部教學講的很不錯,裡面是從實作 Redux 底層的觀念來帶你認識 Redux。我覺得這種循序漸進的方式還蠻不錯的,所以才會寫下這篇來記錄。
如果你已經學過 Redux 的話,這篇應該就不難懂,沒學過的話可能會需要多花一點時間來理解。
總而言之,讓我們馬上來試試看吧。
從 globalState 開始
首先先建立一個 globalState.js
,它的用途就是拿來放置 state,所以 Component 只要透過這個檔案來拿就好了。
最基本的內容如下:
1 2 3 4 5 6 7
| export let globalState = { counter: 0, };
export const setGlobalState = (newState) => { globalState = newState; };
|
接著只要在對應的 Component 引入 globalState
和 setGlobalState
就可以對這裡面的東西做存取或操作。
這邊先說明一下組件結構,大概是長這樣:
Counter
負責顯示數字,Button
則是負責用來改變 state 的按鈕。
接著就透過 globalState
來串接到 Component,所以這時候兩者的內容大概會如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import React, { Component } from "react"; import { globalState } from "./globalState";
export default class Counter extends Component { state = { counter: globalState.counter, }
render() { return <h2>Counter: {this.state.counter}</h2> } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import React, { Component } from "react"; import { setGlobalState } from "./globalState";
export default class Button extends Component { handleClick = () => { setGlobalState({ counter: Math.random() }) }
render() { return <button onClick={this.handleClick}>add</button> } }
|
做到這邊以後,目前的情況應該是這樣:
- 畫面會顯示 Counter: 0
- 按下按鈕後會觸發
setGlobalState
更新 globalState
只不過有一個問題,就是畫面不會更新。
原因很簡單,雖然我們確實是改變了 globalState
裡的值,可是又沒有改變 Component 裡面的 state,所以當然不會觸發 re-render。
那該怎麼辦?我們可以在 globalState
加上幾個東西:
subscribe
notifyAll
關於這兩個東西的用途,我直接寫在註解裡:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const callbacks = [];
export let globalState = { counter: 0, };
const notifyAll = () => { for (let i = 0; i < callbacks.length; i++) { callbacks[i](globalState); } };
export const subscribe = (callback) => { callbacks.push(callback); };
export const setGlobalState = (newState) => { globalState = newState; notifyAll(); };
|
簡單來說,Component 可以透過 subscribe
來訂閱,訂閱的意思就是「當 globalState
改變的時候,通知我一聲」。那要怎麼通知?JS 寫久的話應該都不陌生,就是用 callback 嘛,所以 subscribe
這個 function 要做的事就是:
- 接收一個 callback
- 把這個 callback 儲存起來(放到 callbacks 這個陣列)
而 notifyAll
要做的事情只是把所有 callback 執行一遍而已,並且把把最新的 globalState
當作參數傳進去,接著 Component 就可以透過 callback 拿到。
所以呢,這邊只要修改一下 Counter
就好:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import React, { Component } from "react"; import { globalState, subscribe } from "./globalState";
export default class Counter extends Component { state = { counter: globalState.counter, }
componentDidMount() { subscribe(this.updateCounter); }
updateCounter = newState => { this.setState(newState); };
render() { return <h2>Counter: {this.state.counter}</h2> } }
|
做到目前為止,畫面應該要可以隨著 globalState
做更新,有任何問題的話可以到我寫的範例參考。
認識 HOC(Higher Order Component)
剛剛的例子已經把 state 和 Component 給串起來了,不過有個問題是「每一個 Component 如果都要自己加上 state = {...}
和 subscribe
的話」,其實還蠻點麻煩的,所以這邊就要用 HOC 的概念來改寫剛剛的範例。
附註:HOC 其實就是用 Component 包住後再把 props 傳下去而已,不用想得太複雜。
先來寫一個 utils
,用來包裝用的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { globalState, subscribe } from "./globalState" import React, { Component } from "react"
export function connect (Comp) { class Hoc extends Component { state = { counter: globalState.counter } componentDidMount () { subscribe(this.updateState) } updateState = newState => { this.setState(newState) } render () { return <Comp {...this.state} /> } } return Hoc }
|
接著只要把會用到 globalState
的 Component 包裝起來就好了:
1 2 3 4 5 6 7 8 9 10 11
| import React, { Component } from "react"; import { connect } from "./HocCreator"
class Counter extends Component { render() { return <h2>Counter: {this.props.counter}</h2> } }
export default connect(Counter);
|
透過指令來修改 state
現在又有一個新的問題,就是「如果被別人改到 state 怎麼辦」,像這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import React, { Component } from "react"; import { setGlobalState } from "./globalState";
class OtherGuysComponent extends Component { render() { return <button onClick={() { setGlobalState({ text: '12345' }) }}>change global state</button> } }
export default connect(OtherGuysComponent);
|
這樣 counter 就會在 setGlobalState
的時候被覆寫掉,然後就消失了。
所以要解決這個問題,可以改成「透過指令」的方式來更新 state(如果你學過 redux 的話,其實這就是 dispatch 的概念)
實作方法也不難,我們預期 Component 會這樣子用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import React, { Component } from "react"; import { setGlobalState } from "./globalState";
class OtherGuysComponent extends Component { render() { return <button onClick={() { // 傳入一個 action,裡面包含 type 跟 payload 的資訊 dispatch({ type: 'UPDATE_TEXT', payload: '12345' }) }}>change global state</button> } }
export default connect(OtherGuysComponent);
|
接著回到 globalState 的部分來實作 dispatch
這個 function:
1 2 3 4 5 6 7 8 9 10 11
| export const dispatch = (action) => { if (action.type === 'UPDATE_TEXT') { globalState.text = action.payload } else if (action.type === 'ADD_COUNTER') { globalState.counter = globalState.counter + action.payload } notifyAll(); };
|
接下來是 actionType 和 action creator 的優化,就不特別寫出來了,忘記的話可以看 這篇 來複習。
最後一塊拼圖 reducer
其實就是把剛剛的東西做最後優化,先來複習一下剛剛的進度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export let globalState = { text: '12345', counter: 0, };
export const dispatch = (action) => { if (action.type === 'UPDATE_TEXT') { globalState.text = action.payload } else if (action.type === 'ADD_COUNTER') { globalState.counter = globalState.counter + action.payload } notifyAll() };
|
我們希望優化成這樣子:
1 2 3 4 5 6
| export const dispatch = (action) => { globalState = reducer(globalState, action); notifyAll() }
|
所以換句話說 reducer 的作用就是:
1 2
| newState = reducer(currentState, action);
|
所以最後只要把 reducer 實作出來就好了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function reducer (state, action) { switch (action.type) { case ADD_COUNTER: return { ...state, counter: state.counter + action.payload } case UPDATE_TEXT: return { ...state, text: action.payload } default: return state } }
|
好,關於實作的部分到這邊就完成了!同時你也學會了 redux 的幾個核心理念:
- store
- action
- reducer
- dispatch
經過實作以後,有更清楚整個來龍去脈,我覺得還蠻不錯的。如果想參考最後的成果可以到 這邊 看。