EP08 | React 18 全家桶複習 - Redux

本文使用的是 Redux Toolkit。

架構模式

作用: 透過一個單獨的 state 對象保存整個應用的狀態,這個 state 對象不能被直接修改,而是當某些數據發生變化後,透過 actions 及 reducers 來創建新的對象。

流程:

Redux

下載: npm install @reduxjs/toolkit

官方: Redux Toolkit is our official, opinionated, batteries-included toolset for efficient Redux development. It is intended to be the standard way to write Redux logic, and we strongly recommend that you use it.

項目結構

1
2
3
4
5
6
7
8
9
10
11
|- package.json
|- ...
|- public
|- src -|- index.js
| |- App.js
| |- routes.js
| |- ...
| |- redux -|- store.js
| |- reducers - todoReducer.js
| |- actions - todoAction.js
| |- constants - todo.js

功能: 當 store 內儲存的狀態改變時,使用該狀態的組件資料會相應改變。

代碼範例講解

  1. 透過 createStore(reducer) 讓資料能夠統一管理。
1
2
3
4
5
// 1. 讓 redux 可以管理 state
import { createStore } from 'redux';
import todoReducer from './reducers/todoReducer';
let store = createStore(todoReducer);
export default store;
  1. 創建常量值(目的是避免名稱寫錯)
1
2
3
4
5
// 2. 設定好常數的內容,通常是 action.type 的值
// 目的是確定代碼寫入時,不會發生寫錯的事情
export const ADD_TODO = 'add_todo';
export const DELETE_TODO = 'delete_todo';
export const GET_TODO = 'get_todo';
  1. 創建 reducer 的執行內容
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
//3. 制定好 reducer 的內容(state 經過 dispatch 後要執行的代碼)
import { ADD_TODO, DELETE_TODO, GET_TODO } from '../constants/todo';
const initialState = [
{
userId: 1,
id: 1,
title: 'delectus aut autem',
completed: false,
},
];
// 每個 Reducer 都接收 2 個參數,第一個是目前 state 狀態,第二個是 action
// action 是在執行 dispatch 是用來,告訴 reducer 確切要執行哪些代碼
export default function todoReducer(preState = initialState, action) {
// action 實際為 {type, payload}
// 透過 type 可以決定執行哪個部分,payload 可以知道 dispatch 時是否有傳入參數
// 重點: 遵循 react 的理念,不要直接更改 state 的值
// 透過創建一個新值來複製舊值,所有動作都是對新值做,最後再將新值返回給 state
let newState = [...preState];
// 利用 switch 來指定動作
switch (action.type) {
case ADD_TODO:
newState = [...preState, action.payload];
break;
case DELETE_TODO:
newState = newState.filter((item) => item.id !== action.payload.id);
break;
case GET_TODO:
break;
default:
break;
}
return newState;
}
  1. 自動化生成 action 值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//4. dispatch 的參數就是 action,透過 action 來告訴 reducer 要做甚麼事
// 如果 action 的內容太過複雜,可以透過設定一個檔案專門來放置
import { ADD_TODO, DELETE_TODO } from '../constants/todo';
export function addTodo(payload) {
// action 必定是一個 object ,所以一定要返回 object
return {
type: ADD_TODO,
payload,
};
}
export function deleteTodo(payload) {
console.log('@action', payload);
// action 必定是一個 object ,所以一定要返回 object
return {
type: DELETE_TODO,
payload,
};
}
  1. 讓需要使用儲存庫(store)的組件訂閱資料庫
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Link, useMatch, Navigate, Outlet } from 'react-router-dom';
import store from './01.redux/store';
export default function App() {
// 5. 讓App組件訂閱 store 內容
// 8. 當 state 發生變化時,就會觸發訂閱
store.subscribe(() => console.log('@App訂閱的資料更新啦', store.getState()));
return (
<div className='App'>
{/* 取得 state 的數據*/}
<Link to='/home'>首頁</Link>
<Link to='/search'>搜索</Link>
{useMatch('/') ? <Navigate to='/home' replace={true} /> : <Outlet />}
</div>
);
}
  1. 使用 store ,並透過 dispatch 觸發更新,並使用 useState 來做組件的資料管理以便更新視圖。
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
// 6. 通過 dispatch 讓組件載入所要取得的狀態
import { useState } from 'react';
import store from './01.redux/store';
import { deleteTodo } from './01.redux/actions/todoAction';
export default function Home() {
//7. 透過 store.getState() 取得需要的資料
//為了在 store 中因為狀態的改變而引起視圖的更新,必須透過 useState 接收及改變
const [todo, setTodo] = useState(store.getState());

// 5.讓 Home 組件訂閱 store 內容
store.subscribe(() => {
console.log('@home訂閱的資料更新啦');
// 12. 從store中取得更新後的state
// 組件中的試圖要做更新必須使用 setTodo 將就資料更新成最新的從 store 那邊來的資料
setTodo(store.getState());
});

const deleteItem = (item) => {
// 9. 執行刪除時調用 dispatch(action) 方法
// 10. deleteTodo() 會自動生成一個 action
store.dispatch(deleteTodo(item));
};

return (
<div>
我是Home,放置所有 todo 資料
<hr />
{todo.map((item) => (
<li key={item.id}>
{item.title}
<button
onClick={(e) => {
deleteItem(item);
}}>
刪除
</button>
</li>
))}
</div>
);
}

redux-thunk 異步更新 state

先用一個錯誤寫法說明:
直接使用異步函數,並無法正確產生 action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { GET_TODO } from '../constants/todo';
// 透過異步請求數據

// 錯誤示範
// 返回的不是 object 而是 'undefined'
// 如果使用 async await 錯誤會顯示 Promise
export function getTodos() {
fetch('https://jsonplaceholder.typicode.com/todos/10')
.then((res) => res.json())
.then((json) => {
// 因為是異步函數,所以會先執行後面的代碼,當執行完這個異步時,getTodos早就先返回 undefined
return {
type: GET_TODO,
payload: json,
};
});
// 因為接收者需要一個返回值,但此時還沒有返回值,默認情況下會返回 undefined
// return undefined
}

正解:使用 redux-thunk

redux-thunk 工作流程

  1. 先下載
1
npm install redux-thunk
  1. 使用 redux-thunk,並讓 redux-thunk 與 redux 產生關聯(applyMiddleware)
1
2
3
4
5
6
7
import { applyMiddleware, createStore } from 'redux';
import todoReducer from './reducers/todoReducer';
import thunkMiddleware from 'redux-thunk';
// 1. 讓 redux 可以管理 state
//a. 為了能使用 redux-thunk 執行異步,必須先加入
let store = createStore(todoReducer, applyMiddleware(thunkMiddleware));
export default store;
  1. action 中透過返回異步函數,讓 redux-thunk 知道他要開始工作了
1
2
3
4
5
6
7
8
9
10
11
12
// b. 正確方法: 使用 redux-thunk
export function getTodos() {
console.log('@action getTodos 1');
// 在action中返回函數,讓 thunk 知道開始工作
return async (dispatch, getState) => {
const res = await fetch('https://jsonplaceholder.typicode.com/todos');
const todos = await res.json();
console.log('@action getTodos 2', todos);

dispatch({ type: GET_TODO, payload: todos });
};
}
  1. 在組件中使用的方式跟一般觸發更新 store 相同,都是用 dispatch
1
2
3
4
5
6
7
8
9
10
11
//Home.js
useEffect(() => {
// 判斷是否存在數據,可以減少數據重複請求
if (store.getState().length === 0) {
// 沒有數據,所以向 redux 發出請求數據
console.log('@home 發出數據請求');
store.dispatch(getTodos());
} else {
console.log('@home緩存');
}
}, []);

多個 reducer 的載入方法

  1. 創建新的 reducer。(actionCreator 可創可不創)
1
2
3
4
5
6
7
8
9
10
11
12
// searchReducer.js
// A. 創建另一個 reducer
import { SEARCH_TEXT } from '../constants/todo';
const initialValue = '';
export default function searchReducer(preState = initialValue, { type, payload }) {
switch (type) {
case SEARCH_TEXT:
return payload;
default:
return '';
}
}
  1. 在 store.js 中使用 combineReducers 把多個 reducer 做整合用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// store_combineReducers.js
import { createStore, combineReducers } from 'redux';
import todoReducer from './reducers/todoReducer';
// B. 引入第二個 reducer
import searchReducer from './reducers/searchReducer';
// C. 使用多個 reducer,需要借用 redux 的 combineReducers 方法作整合
// combineReducers({自訂 reducer名: 對應的 reducer})
// 在組件中引用時,store.getState() 就會是一個物件如下:
// {自訂 reducer名: 對應 reducer 的狀態值}
const reducers = combineReducers({
todos: todoReducer,
searchText: searchReducer,
});
// D. 將多個 reducer 結合後,透過傳給 createStore 來變成一個共同的庫
let store = createStore(reducers);
export default store;
  1. 組件中使用,一樣透過 store.getState() 取得所有狀態,不過多個 reducers 的狀態呈現會是一個物件。
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
import { useMemo, useState } from 'react';
import store from './01.redux/store_combineReducers';
export default function SearchItem() {
const [searchRes, setSearchRes] = useState(store.getState().searchText);
// F. 使用多個 reducer 的話,狀態會變成一個物件 {指定的 reducer名: 對應的state}
store.subscribe(() => {
console.log('@SearchItem 訂閱資料', store.getState());
// 在狀態有改變時,就會調用 setSearchRes 來讓頁面有所更新
setSearchRes(store.getState().searchText);
});
// useMemo 的目的是在其他狀態有更新時,可以透過緩存取得值,無須耗費效能重新計算
const searchTodos = useMemo(
() => store.getState().todos.filter((todo) => todo.title.includes(store.getState().searchText)),
[searchRes]
);

return (
<ul>
{searchTodos.length ? (
searchTodos.map((todo) => <li key={todo.id}>{todo.title}</li>)
) : (
<li>無搜索資料</li>
)}
</ul>
);
}

redux 的開發者工具

redux-devtools

  1. (Chrome 版本) 在線上應用程式商店下載 Redux DevTools
  1. 使得專案可以使用 Redux DevTools。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// store.js
import { applyMiddleware, createStore, compose } from 'redux';
import todoReducer from './reducers/todoReducer';
import thunkMiddleware from 'redux-thunk';

// A. 使用開發者工具
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

let store = createStore(
todoReducer,
// B. 讓開發者工具能夠結合 store
composeEnhancers(applyMiddleware(thunkMiddleware))
);
export default store;

如此一來就可以更直觀的觀察到 redux 數據的變化。