用實作的方式來重新學習 Redux

再次突破眼界。

簡述

在我們要跨組件溝通時,很常會愈到「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 引入 globalStatesetGlobalState 就可以對這裡面的東西做存取或操作。

這邊先說明一下組件結構,大概是長這樣:

  • App
    • Wrapper
      • Counter
      • Button

Counter 負責顯示數字,Button 則是負責用來改變 state 的按鈕。

接著就透過 globalState 來串接到 Component,所以這時候兩者的內容大概會如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Counter
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
// Button
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>
}
}

做到這邊以後,目前的情況應該是這樣:

  1. 畫面會顯示 Counter: 0
  2. 按下按鈕後會觸發 setGlobalState 更新 globalState

只不過有一個問題,就是畫面不會更新

原因很簡單,雖然我們確實是改變了 globalState 裡的值,可是又沒有改變 Component 裡面的 state,所以當然不會觸發 re-render。

那該怎麼辦?我們可以在 globalState 加上幾個東西:

  1. subscribe
  2. 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
// 所有訂閱 globalState 的人傳進來的 function
const callbacks = [];

export let globalState = {
counter: 0,
};

// 把所有人的 callback 執行一遍,並把更新後的 state 傳進去
const notifyAll = () => {
for (let i = 0; i < callbacks.length; i++) {
callbacks[i](globalState);
}
};

// 要訂閱的人就傳一個 callback 進來
export const subscribe = (callback) => {
callbacks.push(callback);
};

export const setGlobalState = (newState) => {
globalState = newState;
// 更新後通知所有訂閱者
notifyAll();
};

簡單來說,Component 可以透過 subscribe 來訂閱,訂閱的意思就是「當 globalState 改變的時候,通知我一聲」。那要怎麼通知?JS 寫久的話應該都不陌生,就是用 callback 嘛,所以 subscribe 這個 function 要做的事就是:

  1. 接收一個 callback
  2. 把這個 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,
}

// mount 後訂閱
componentDidMount() {
subscribe(this.updateCounter);
}

// 收到通知時,要執行的 callback
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
// HocCreator.js
import { globalState, subscribe } from "./globalState"
import React, { Component } from "react"

// 接收一個 Component
export function connect (Comp) {
// smart component
class Hoc extends Component {
state = {
counter: globalState.counter
}
componentDidMount () {
subscribe(this.updateState)
}
updateState = newState => {
this.setState(newState)
}
// 把 state 當作 props 傳下去
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 {
// 現在只要從 props 把 state 拿出來用就行了
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";

// 假設原本的 state 長這樣:
// globalState: {
// counter: 0,
// text: 'abc'
// }

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,裡面包含 typepayload 的資訊
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
// globalState.js
export const dispatch = (action) => {
// 根據 action type 來更新部分的 state
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) => {
// 根據 action type 來更新部分 state
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) => {
// 加上 reducer
globalState = reducer(globalState, action);
// 發通知
notifyAll()
}

所以換句話說 reducer 的作用就是:

1
2
// 把 state 跟 action 丟進去以後,回傳新的 state
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

經過實作以後,有更清楚整個來龍去脈,我覺得還蠻不錯的。如果想參考最後的成果可以到 這邊 看。

快速入門 generator Ant Design-表單相關元件
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×