# Redux Redux 是一種前端的「架構模式」。簡單來說,就是用來實現「狀態管理機制」的套件,並且能套用在任何程式語言。 ## 認識 Redux 之前 最早是在 Facebook 提出 React 時,所提出的 Flux 開發架構,目的是解決 MVC 在大型商業網站所存在的問題,例如管理 UI 畫面與資料之間的對應關係。 ### 「狀態管理」是一門學問 從以前到現在,關於「狀態管理」,在不同框架也會有不同處理方式,舉例來說: * jQuery:資料與畫面分離,重點放在「畫面」上 * Vue 與 Angular:資料與畫面「雙向綁定」 * React:只需要管資料,藉由「狀態」渲染出畫面 ## Flux 簡介 在傳統的 MVC 架構,Model 和 View 之間可能會呈現複雜關係 ![](https://hackmd.io/_uploads/HJdFy4Hi2.png) Facebook 當初之所以會提出 React 和 Flux,就是為了解決當 APP 架構變更大,功能變更複雜時遇到的問題。例如 FB 中的 Messager 的提醒通知,程式碼中可能有許多部分會操控到同一 Model,使得狀態過於複雜,不易進行後續追蹤。 Flux 架構流程如下,若想要改變 Store(資料)或 View(畫面),都必須透過 Dispatcher 發出 Action(指令),呈現單向資料流: ![](https://hackmd.io/_uploads/ByKiyNBjn.png) 上述架構,在小型專案中其實是多此一舉,直接透過修改 Store 去渲染 View 即可;但在大型專案中,透過像這樣「集中」管理的方式,會更容易進行複雜的狀態管理。 ## 為何需要Redux? Redux 是個函式庫,是一個全域的狀態管理工具,能管理整個網站需要的 State。 當所有的資料都放在 Redux,需要資料的 Component 可直接從 Redux 取得,所以組件管理 state 變得更方便,而且也確保整個專案的資料都來自同一個地方。 透過 Redux 也可以將操作元件 state 的邏輯部分從元件抽離出來,達到元件做純渲染,Redux 處理資料邏輯的結果。 **主要功能: 管理全域狀態、處理資料邏輯** ![image alt](https://static.coderbridge.com/img/andyTsai2321/9e24368702e648688563c05c6acf0128.png) ## Redux Data Flow 了解 State、Action、Reducer、Store 與其資料流動。 * **State** : 用來儲存整個應用程式的資料 * **Action** : 要改變 State 唯一的方式就是指派一個 Action,而 Action 本身就只是一個 Object {type,paload},但 Action 不會直接修改 State,而是交由 Reducer 來處理 * **Reducer** : Reducer 是一個 pure function,它會取得先前的 State 和一個 Action,並根據傳入的 Action 的 type 去將 State 值做改變,最後回傳的是一個經過計算後新的 State 物件。 由下圖所示 1. State 的初始資料會渲染至畫面 1. 畫面點擊後,執行事件,經由 Dispatch 這個方法,將 Action,傳給 Store 1. 在 Store 下,找到相對應的 Reducer 1. Reducer 讀取 Action 物件,並執行相對應的動作 1. 成功更改 State 資料 ![image alt](https://d33wubrfki0l68.cloudfront.net/01cc198232551a7e180f4e9e327b5ab22d9d14e7/b33f4/assets/images/reduxdataflowdiagram-49fa8c3968371d9ef6f2a1486bd40a26.gif) # Redux Toolkit [參考](https://ithelp.ithome.com.tw/m/articles/10306762) ## 為何需要 Redux Toolkit Redux Toolkit 是一個可以幫助你更有效率撰寫 Redux 的一個 library,它提供了一些 API 讓你更方便的建立 Store、Actions 和 Reducers。 ## 需要先知道的 api ### configureStore() 功用和 createStore 一樣可以建立 Store,但還可以結合 reducers、middleware。 ```javascript= import { configureStore } from '@reduxjs/toolkit' import rootReducer from './reducers' const store = configureStore({ reducer: rootReducer }) ``` ### createAction() 建立 action creator 的函式。放在 createAction() 裡面的參數會自動變成 action type 字串常數。 ```javascript= import { createAction } from '@reduxjs/toolkit'; const fetchTodos = createAction('todos/fetchTodos'); // { type: 'todos/fetchTodos' } const setFilter = createAction('filter/setFilter'); // setFilter('All') // returns { type: 'filter/setFilter', payload: 'All' } ``` ### createReducer() 使用它在撰寫 reducer 的時候可以不用再用 switch case 語法,此外,它會自動使用 immerjs 讓您更簡單的處理狀態更新,例如 state.todos[3].completed = true。 (沒有使用 immutable 相關套件時,這樣的寫法會有 side-effect)。 ```javascript= import { createAction, createReducer } from '@reduxjs/toolkit'; const setFilter = createAction('filter/setFilter'); const initialState = 'All'; const filterReducer = createReducer(initialState, (builder) => { builder .addCase(setFilter, (state, action) => { state = action.payload; }); }) ``` ### createSlice() createSlice 將 slice name、initial state、reducer、action 集中建立,在slice 檔案中。 createSlice 內部整合了 createReducer 和 createAction,因此 在大部分應用中, 不需特別寫這二個函式,只要使用 createSlice 就足夠。 使用 createSlice 建立完成後,createSlice 會自動生成 action creators 及 reducer,就可以直接使用或是導出。 * name:字串,被用於生成的 action type 的前綴 (eg. filter/setFilter) * initialState: 初始狀態值 * reducers:創建改變 state 狀態的函式,會有兩個參數state和action,state就是讓你修改狀態,你可以直接對 state 進行操作;action就是讓你傳入參數 ```javascript= import { createSlice } from '@reduxjs/toolkit'; const initialState = 'All'; const filterSlice = createSlice({ name: 'filter', initialState, reducers: { setFilter(state, action) { state = action.payload }, }, }); export const { setFilter } = filterSlice.actions; export default filterSlice.reducer; ``` ### createAsyncThunk 使用 createAsyncThunk 製作非同步 action creator createAsyncThunk 接受三個參數 * 一個產生 action types 的 string (會帶有 slice name 做為前綴) * 一個回傳 promise 的 callback function(第一參數為外傳參數,第二參數則是thunkAPI) * 一個是判斷情況 condition * 在 thunkAPI 中可以取得 dispatch、getState、extra、 rejectWithValue 等 ```javascript= import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' import { todosAPI } from "../../api"; // 這邊要導出,因為它不會納至 todoSlice.actions 中 export const fetchTodos = createAsyncThunk("todos/fetchTodos", async () => { const response = await todosAPI.fetchTodos(); return response; }); // 第一參數 userId 為外部傳入,第二參數則是 thunkAPI const fetchUserById = createAsyncThunk('users/fetchByIdStatus', async (userId, thunkAPI) => { const response = await userAPI.fetchById(userId); return response.data; }); const fetchUserById = createAsyncThunk( 'users/fetchByIdStatus', async (userId, thunkAPI) => { const response = await userAPI.fetchById(userId); return response.data; }, { // 停止重複執行的條件,當已經獲取 prod 或是 正在載入或仔入成功時,則停止執行 fetch 的動作 condition: (_, thunkAPI) => { if ([loadingStatus.pending, loadingStatus.succeeded].includes(loading) || prod.length) { return false; } }, } ); ``` 使用 createAsyncThunk 建立的非同步 action creators,就會自動產生下面的對應 ```javascript= // promise pending 等待中 fetchTodos.pending(); // action type => 'todos/fetchTodos/pending' // promise fulfilled 正確完成 fetchTodos.fulfilled(); // action type => 'todos/fetchTodos/fulfilled' // promise reject 已拒絕,操作失敗 fetchTodos.rejected(); // action type => 'todos/fetchTodos/rejected' ``` 由於這些 action 並不是在 slice 中被定義,所以如果要在 createSlice 中監聽這些 action type,需要在 extraReducers 中透過 builder.addCase 來使用 ```javascript= const todosSlice = createSlice({ name: "todos", initialState, reducers: { // 一般 reducer fuction 定義的地方 ... }, // 加入額外 reducer 的地方 // 使用 createAction 或 createAsyncThunk 建立的 action creators 都會設定在此 extraReducers: (builder) => { builder .addCase(fetchTodos.pending, (state, action) => { state.isLoading = true; }) .addCase(fetchTodos.fulfilled, (state, action) => { state.data = action.payload.data.map(({ id, title, completed }) => { return { id, text: title, completed }; }); state.isLoading = false; }); } ``` ## 範例 [使用 createSlice 的 todolist](https://codesandbox.io/s/react-todomvc-redux-toolkit-nk0nsh?file=/src/store/slices/todosSlice.js) [使用 createSlice 、 createAsyncThunk 的 todolist](https://codesandbox.io/s/react-todomvc-redux-toolkit-api-b4ob97?file=/src/store/slices/todosSlice.js:22-38) ## Usage with TypeScript [reference](https://react-redux.js.org/using-react-redux/usage-with-typescript#define-typed-hooks) ### Define Root State and Dispatch Types ```javascript= import { configureStore } from '@reduxjs/toolkit' // ... const store = configureStore({ reducer: { posts: postsReducer, comments: commentsReducer, users: usersReducer, }, }) // Infer the `RootState` and `AppDispatch` types from the store itself export type RootState = ReturnType<typeof store.getState> // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} export type AppDispatch = typeof store.dispatch ``` ### Define Typed Hooks 對於 useDispatch,默認的 Dispatch 類型不知道 thunk 或其他中間件。 為了正確調度 thunk,您需要使用 store 包含 thunk 中間件類型的特定自定義 AppDispatch 類型,並將其與 useDispatch 一起使用。 添加預鍵入的 useDispatch 掛鉤可防止您忘記在需要的地方導入 AppDispatch。 ```javascript= // app/hooks.ts import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' import type { RootState, AppDispatch } from './store' // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch: () => AppDispatch = useDispatch export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector ``` # Redux Toolkit 進階 - createSlice 內有使用 immer 因此直接使用sort()排序是不會有問題的。其原因是因為sort()會直接變更原來的陣列,但是state是不允許直接變更,但因為有 immer 這個函式庫幫忙,才避免出錯。