初探 Redux

做了好多次練習。

簡述

這邊會跟著 Redux 官方文件 來跑一次流程。

在介紹之前,讓我在強調一次:

  • Redux 並沒有跟 React 綁在一起
  • Redux 並沒有跟 React 綁在一起
  • Redux 並沒有跟 React 綁在一起

我在學之前也以為他是專屬於 React 的東西,畢竟是以「Re」開頭的嘛。

總之呢,它只是一個基於 flux 打造的 library,用來「管理狀態」。你可以搭配 Vanilla JS 來用,或甚至是別的程式語言也可以。

從四大要素開始

這邊先簡單介紹 Redux 裡面幾個主要的角色:

  • store(透過 reducer 來建立)
  • reducer(跟 array 的 reduce 概念很相似)
  • action(一個 Object,會有 type 跟 payload)
  • dispatch(透過它來發出 action 給 reducer)

等一下的範例會一一介紹它們是幹嘛用的,廢話不多說,開始吧!

這邊的範例很簡單,只需要寫一隻檔案就行了,所以直接附上 code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import { createStore } from 'redux';

// state(初始值)
const initState = {
todos: []
}

let id = 0;

// reducer
function todosReducer (state = initState, action) {
switch (action.type) {
// action
case "addTodo":
return {
...state,
todos: [
...state.todos,
{
id: id++,
name: action.payload.name,
}
]
}
// action
case "deleteTodo":
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload.id)
}
default:
return state
}
}

// store(用 reducer 建立)
const store = createStore(todosReducer);

// 這個待會再解釋
store.subscribe(() => {
console.log('Change!');
console.log(store.getState());
})

// 下面都是 dispatch
store.dispatch({
type: 'addTodo',
payload: {
name: 'todo1'
}
})

store.dispatch({
type: 'addTodo',
payload: {
name: 'todo2'
}
})

store.dispatch({
type: 'addTodo',
payload: {
name: 'todo3'
}
})

store.dispatch({
type: 'deleteTodo',
payload: {
id: 1
}
})

要建立一個 store 的第一步是先寫好「reducer」,這個範例的 reducer 是 todosReducer 這個 function。

簡單來說,reducer 就是一個用來「產生 state」的東西,只要給他對應的 「action」,它就吐給你對應的 state。

至於 reducer 裡面要做什麼處理是我們自己決定的,像是我們希望接收到 addTodo 這個 action 時,就新增一筆 todo 到 state 裡面,所以才會有這段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
switch (action.type) {
case: 'addTodo':
return {
...state,
todos: [
...state.todos,
{
id: id++,
name: action.payload,
}
]
}
// ...
}

後面的 deleteTodo 也是以此類推。

定義好 reducer 以後,只要把它丟到 Redux 提供的 createStore,store 就建立好了,就是這麼簡單。

1
const store = createStore(todosReducer);

接下來,每當我想要對 store 裡面的東西做事情,就得透過「dispatch」+「action」才可以:

1
2
3
4
5
6
store.dispatch({
type: 'addTodo',
payload: {
name: 'todo1'
}
})

這一段的意思就是說「我想執行 addTodo 這個 action」,麻煩幫我 dispatch(指派)給 reducer。

眼尖一點就會注意到 action 其實只是一個 Object,裡面會放 typepayload 這兩個 key,代表我想做的事情跟額外資訊。

所以當我 dispatch 這個 action 以後,reducer 就會吐給我新的 state,它應該要長的像這樣:

1
2
3
4
5
{ 
todos: [
{ id: 0, name: 'todo1' }
]
}

以上就是最基本的流程,沒有很複雜,本質就是這樣而已。

至於這一段:

1
2
3
4
store.subscribe(() => {
console.log('Change!');
console.log(store.getState());
})

其實就跟 addEventLisener() 87 分像,它的意思是「當 state 改變的時候幫我 call 這個 function」:

1
2
button.addEventListender(() => ...);
store.subscribe(() => ...);

就這樣而已。

最後是做個補充,當我們在 Reducer 裡面更新 state 時,一定要用 Immutable 的方式來改變,這邊先示範一個錯誤的範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const initState = {
// 現在多加一個 email
email: '12345@gmail.com',
todos: []
}

function reducer(state, action) {
switch (action.type) {
case: 'addTodo'
// 忽略了 email 的部分
return {
todos: [
...state.todos,
{name: action.payload.name}
]
}
default:
return state
}
}

這樣子更新後的 state 就會變成:

1
2
3
{
todos: [{ name: 'xxx'}]
}

為什麼?我說過一定要用 Immutable 的方式來改變 state,而剛剛在 reducer 裡面回傳的只有 todos,所以 email 就消失了。

正確的做法應該是這樣子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const initState = {
email: '12345@gmail.com',
todos: []
}

function reducer(state, action) {
switch (action.type) {
case: 'addTodo'
return {
// 先複製原本的 state
...state,
// 再去改我想要改的 state
todos: [
...state.todos,
{name: action.payload.name}
]
}
default:
return state
}
}

這就跟在用 useState 的概念是一樣的,不要忘記囉!

來做點優化,加上 action type 與 action creator

前面雖然已經介紹過 Redux 的基本用法,不過應該能注意到幾個小問題:

  1. action 是用「純字串」來寫的,那打錯字怎麼辦?
  2. 每次 dispatch 都要傳一包 Object 是不是有點太 hard code 了?

Action Type

首先是第一個問題,這其實蠻困擾的,因為假設我哪天打錯字的話:

1
2
3
4
5
6
7
store.dispatch({
// 多一個 s
type: 'addTodos'
payload: {
name: 'PeaNu'
}
})

這樣是不會出跳出任何錯誤的,因為對 reducer 而言 addTodos 只是一個不存在的 case,所以只會跳到 default 區塊而已。但這樣麻煩可就大了,因為你可能根本不知道是自己打錯字的關係。

所以更好的做法是改用「Action Type」,其實就只是建立一個 constant 啦:

1
2
3
4
const actionTypes = {
ADD_TODO: 'addTodo',
DELETE_TODO: 'deleteTodo'
}

接著就可以去把原本的 action 修改成這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 1. reducer 裡的 action 
function todosReducer (state = initState, action) {
switch (action.type) {
// 新增 todo
case actionTypes.ADD_TODO:
return {
...state,
todos: [
...state.todos,
{
id: id++,
name: action.payload.name,
}
]
}
// 刪除 todo
case actionTypes.DELETE_TODO:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload.id)
}
default:
return state
}
}

// 2. dispatch 的 action,其他以此類推
store.dispatch({
type: actionTypes.ADD_TODO,
payload: {
name: 'todo1'
}
})

現在把 action 都改成變數以後,如果我又打錯字,系統就會直接噴 Error 跟我說Action may not have a undefined type property 之類的,不會再有原本那種找不出 bug 的麻煩。

Action creator

接著是第二個問題,如果每次 dispatch 都要這樣寫的話真的蠻麻煩的:

1
2
3
4
5
6
store.dispatch({
type: actionTypes.ADD_TODO,
payload: {
name: 'todo1'
}
})

如果可以寫成這樣的話世界是不是會更好?

1
store.dispatch(addTodo('todo1'));

有辦法做到嗎?其實剛剛有暗示過,你只要想想 action 的本質,就會發現要做到這點並不困難。

剛剛說過,action 的本質只是一個 Object,所以我們只要寫一個可以回傳對應 Object 的 function 不就好了嗎?

1
2
3
4
5
6
7
8
9
function addTodo(name) {
// 這邊回傳的東西就是 action(Object)
return {
type: ActionTypes.ADD_TODO,
payload: {
name
}
}
}

這種做法就叫做「Action creator」,講白話一點就是把 action 要傳的內容改用 function 來寫而已。

總之呢,只要改用這種方式優化後,你的 code 就會乾淨許多,也比較好維護。

最後應證一下我最開始說的,Redux 並沒有一定要跟 React 綁在一起用,所以這邊附上一個用 Vanilla JS + Redux 寫的 todo 範例,如果有任何疑惑就去看看吧。

mentor-program-day130 什麼是 Flux?
Your browser is out-of-date!

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

×