終究都要學 React 何不現在學呢? - React Redux - Redux 與快速入門 - (29)

前言

這一篇開始會進入另一個新章節,只是這個章節我只會簡單的拆成兩篇,所以內容相對會少一點,那麼什麼是 Redux 呢?這一篇就讓我們來了解一下吧。

什麼是 Redux?

那麼什麼是 Redux 呢?Redux 其實是一個狀態管理工具,你也可以把它想像成容器,它可以幫助我們管理整個網頁應用程式的狀態,而 Redux 本身是可以獨立使用的。

剛剛有提到 Redux 是專門用於狀態管理的工具並且可以獨立使用,所以在官方首頁上就可以看到一句話:

A Predictable State Container for JS Apps

所以我們可以得知 Redux 本身是可以獨立使用的,但是部分人看到 Redux 會以為它就是專屬於 React 的狀態管理工具,如同 Vue 的 Vuex 一樣,也因為 Redux 本身可以獨立運作的關係,所以 Redux 也可以和 React 以外的框架或是函式庫一起使用,如:Angular、Vue、jQuery 等,除此之外 Redux 也非常小一包,只有 2kb 左右的大小。

或許這時候你可能會想到前面章節所介紹的 useReducer 範例:

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
const reducer = (state, action) => {
switch(action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return { count: 0 }
}
}

const App = () => {
const initialState = {
count: 0,
};

const [state, dispatch] = React.useReducer(reducer, initialState);

return (
<div>
Count: {state.count}
<br />
<button onClick={() => dispatch({ type: "decrement" })}>Decrement</button>
<button onClick={() => dispatch({ type: "increment" })}>Increment</button>
</div>
);
}

const app = document.querySelector('#app');
const root = ReactDOM.createRoot(app);
root.render(<App />);

那麼 useReducer 跟 Redux 有什麼關係呢?本質上來講 useReducer 是 React 官方參考 Redux 後所提供的一個 Hook,但實際上來講兩者能做的事情還是有一定差異的,例如:Redux 可以讓我們在不同的頁面或是不同的元件之間共享狀態,而 useReducer 則是只能在同一個元件之間共享狀態,除非、你有使用到 Context API,否則正常情況下 useReducer 是無法在不同的元件之間共享狀態的。

React Redux 跟 Redux 關係

那麼剛剛有提到 Redux 本身是可以獨立使用的,只是如果可以獨立運作的話,那 React Redux 跟 Redux 有什麼關係呢?其實是因為 React 非常常使用 Redux 來管理狀態,所以 React 官方就提供了一個 React Redux 的套件,這個套件就是將 Redux 跟 React 進行整合,讓我們可以更方便的使用 Redux。

安裝 React Redux 與 Redux Toolkit

接下來我們要來安裝 React Redux 與 Redux Toolkit,這兩個套件都是 Redux 的相關套件,而 Redux Toolkit 則是 Redux 官方提供的一個套件,它可以讓我們更方便的使用 Redux。

那麼為什麼要用 Redux Toolkit 呢?其實你到 Redux Toolkit 官網可以看到有列出三個重點:

  • “Configuring a Redux store is too complicated”
    • 設定 Redux 太複雜
  • “I have to add a lot of packages to get Redux to do anything useful”
    • 必須安裝很多套件才能使用 Redux
  • “Redux requires too much boilerplate code”
    • Redux 需要太多的 boilerplate code

基於上述三點的關係,所以我們這邊會使用 Redux Toolkit 來進行 Redux 的設定,因此我們要先安裝 Redux Toolkit 跟 React Redux:

1
npm install react-redux @reduxjs/toolkit

安裝時要注意一下 React Redux 8.x 至少需要 React 16.8.3 以上的版本才能夠使用 React Hook 唷。

使用 Redux Toolkit 建立 Redux Store

那麼接著這邊將會使用這一份範例程式碼當作範例,建議可以先下載下來這樣子才方便後面的學習。

如果沒有任何問題的話,請先在 src 資料夾下建立一個 store 資料夾,然後在 store 資料夾下建立一個 index.js 檔案,接著在 index.js 檔案中加入以下程式碼:

1
2
3
4
5
6
7
import { configureStore } from "@reduxjs/toolkit";

const store = configureStore({
reducer: {},
});

export default store;

接著打開 src/main.jsx 檔案,將引入 import store from './store'import { Provider } from 'react-redux',並將 <Provider store={store}> 包住 <App />

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
import store from './store'
import { Provider } from 'react-redux'

ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={ store }>
<App />
</Provider>
</React.StrictMode>
)

到目前為止,我們已經完成了 React Redux 的設定,所以接著我們要來調整一下原本 ToDoList 的程式碼,讓它可以使用 Redux 來管理狀態。

使用 Redux Toolkit 建立 Redux Slice

接下來我們要建立 React Slice,所以請在 store 資料夾下建立一個 todoSlice.js 檔案,然後在 todoSlice.js 檔案中引入 createSlice

1
import { createSlice } from "@reduxjs/toolkit";

createSlice 是 Redux Toolkit 提供的一個方法,它可以讓我們更方便的建立 Redux Slice,而 createSlice 會回傳一個物件,裡面包含了 reduceractions,所以我們可以這樣子使用:

1
2
3
const todoSlice = createSlice({
name: "todo"
});

那麼 createSlice 裡面有兩個東西很重要,分別是 initialStatereducersinitialState 是初始的狀態,而 reducers 則是一個物件,裡面包含了所有的 actionaction 你也可以想像成函式,而這些函式會帶入 stateaction 這兩個參數。

接著我們就來把 ToDoList 的程式碼轉換成 Redux 的程式碼,首先我們要先建立 initialState,所以在原本寫在 App.jsx 的 const [ todoList, setTodoList ] = useState(JSON.parse(localStorage.getItem('todoList')) || []); 就會變成 initialState 的值:

1
2
3
4
5
6
7
8
9
import { createSlice } from "@reduxjs/toolkit";

const store = createSlice({
initialState: {
todoList: []
},
reducer: {},
});

接著 addTodo 呢?則會放在 reducer

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createSlice } from "@reduxjs/toolkit";

const todoSlice = createSlice({
name: "todo",
initialState: {
todoList: [],
},
reducers: {
addTodo (state, action) {

}
},
});

接著我們要把原本的 setTodoList 改成 state.todoList.push(action.payload); 即可。

在 Slice 這邊有一件事情很重要,我們在匯出的時候,不是直接匯出 todoSlice,而是匯出 todoSlice.reducer,因為 todoSlice 會包含 reduceractions,而我們只需要 reducer,所以我們要匯出 todoSlice.reducertodoSlice.actions

1
2
export const { addTodo } = todoSlice.actions;
export default todoSlice.reducer;

到目前為止,我們已經把原本的程式碼轉換成 Redux 的程式碼了,接著我們要把 todoSlice 匯出,並且在 src/store/index.js 檔案中引入 todoSlice

1
2
3
4
5
6
7
8
9
10
import { configureStore } from "@reduxjs/toolkit";
import todoSlice from "./todoSlice";

const store = configureStore({
reducer: {
todoSlice,
},
});

export default store;

最後回到 App.jsx 檔案,我們要把原本的 useState 改成 useSelectoruseDispatch,所以要引入 useSelectoruseDispatch

1
import { useSelector, useDispatch } from "react-redux";

接著該如何取得放在 Redux 的 todoList 呢?我們可以這樣子取得:

1
const todoList = useSelector((state) => state.todoSlice.todoList);

至於 addTodo 的話則還必須額外引入 addTodo

1
import { addTodo } from "./store/todoSlice";

那麼這邊就有一個問題發生了,由於在 App.jsx 我們是命名為 addTodo,而在 todoSlice 裡面我們是命名為 addTodo,所以會發生命名衝突,所以我們要把 App.jsx 的 addTodo 改成 handleAddTodo,這樣子才不會影響到 todoSlice,所以目前程式碼變成以下這樣:

1
2
3
4
5
6
7
8
9
const handleAddTodo = (event) => {
dispatch(addTodo({
id: Date.now(),
name: event.target.previousElementSibling.value,
status: false
}
));
event.target.previousElementSibling.value = '';
};

接著原本的 List 是接收著 setTodoList,除此之外也要將原本寫在 List 底下的 updateTodo 改放到 todoSlice 裡面,所以目前程式碼變成以下這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { createSlice } from "@reduxjs/toolkit";

const todoSlice = createSlice({
name: "todo",
initialState: {
todoList: [],
},
reducers: {
addTodo (state, action) {
state.todoList.push(action.payload);
},
updateTodo(state, action) {
const id = action.payload;
const index = state.todoList.findIndex((todo) => todo.id === Number(id));
state.todoList[index].status = !state.todoList[index].status;
},
},
});

export const { addTodo, updateTodo } = todoSlice.actions;

export default todoSlice.reducer;

接著原本在 List 的程式碼就改成以下

1
2
3
4
const handleUpdateTodo = (event) => {
const { id } = event.target.dataset;
dispatch(updateTodo(id));
}

這樣子我們就將原本的程式碼轉換成 Redux 的程式碼了,而這邊也是簡單的快速入門而已,但範例程式碼一樣都放在這邊

後記

本文將會同步更新到我的部落格