# [React] Redux-Toolkit 筆記 ###### tags: `React` `前端筆記` `Udemy 課程筆記` 整個更新資料的流程都與 React-Redux 相同,只是 React-Toolkit 簡化繁瑣的寫法,減低維護的成本。 > 1. 在初始化 store 的時候也會呼叫 rootReducer -> all children reducers -> rootReducer 取得 children reducers 回傳的 state -> 組合成一個巨大的 state 給 store -> 有訂閱的 componetns re-render > 2. component dispatch action 的時候也會經由 rootReducer -> call all children reducers -> rootReducer 取得 children reducers 回傳的 state -> 再組成一個巨大的 state 給 store -> 有訂閱的 componetns re-render ## `createSlice()` -> 每一個區塊的資料都是一個 `slice` 幫助開發者直接建立一組資料,並且同步建立: 1. name 2. inital state 3. `reducers: {}` 在底層有順道叫用 `createAction` 及 `createReduer`,所以會直接建立 actions 4. reducer functions 因為 Redux-Toolkit 有使用 immer 套件的幫助,所以可以直接用 immutable 的寫法改變 state,最後 immer 套件會把 immutable 的寫法改成 mutable(因為底層有使用 `createReducer`) ```javascript= // mutable way: const obj = { x: 1, y: 2, z: { a: 100 } } const obj2 = { ...obj, z: { ...obj.z, b: 200 } } ``` ```javascript= // immutable way const obj = { x: 1, y: 2, z: { a: 100 } } const obj2 = obj obj2.z.b = 200 // 這裡會改動到原本的 obj!! ``` `createSlice` = `createAction` + `createReducer`: - `createSlice` 會找 `reducers: {...reducer methods}` 的 method names 當作 `actions` 內的 `keyName`(`actions` 內是 `action creator`,所以 `postsslice.actions.createpost` 是得到一個 `function`,`postsslice.actions.createpost()` 才會得到 `action object`) ```javascript const postsSlice = createSlice({ name: 'posts', initialState: [], reducers: { createPost(state, action) {}, updatePost(state, action) {}, deletePost(state, action) {}, }, }) console.log(postsSlice) /* { name: 'posts', actions : { createPost, updatePost, deletePost, }, reducer } */ const { createPost } = postsSlice.actions /* 得到 action creator function */ console.log(createPost) // actionCreator() {...} /* 得到 action creator function 回傳的 action object */ console.log(createPost(())) // {type : "posts/createPost", payload : undefined } console.log(createPost({ id: 123, title: 'Hello World' })) // {type : "posts/createPost", payload : {id : 123, title : "Hello World"}} ``` ### 底層被呼叫的 `createAction` 簡易且方便建立 `action` 物件的函式: > 嚴格說起來 `createAction` 只會得到 action creator function(就是 High Order Function 的概念,一個函式回傳另一個函式),不是直接回傳一個 `action` 1. 初次帶入 `action type` 叫用後會回傳 action creator function 2. action creator function 會收一個 parameter,並且會自動放在 `payload` 物件中 ```javascript= /* 一開始 redux 需要「較多手動」的設置 */ /* 因為 store 收到 action 時,rootReducer 會叫用整個 children reducers,因此每個 type 最好都用常數保存,避免重複呼叫不同的 reducer */ const INCREMENT = 'counter/increment' /* 建立一個函式專門建立 action,並開 parameter 讓 payload 可以有動態的數字 */ function increment(amount: number) { return { type: INCREMENT, // 把常數帶入 type,這樣子就可以依照常數的特性,不會不小心建立同個 type payload: amount, } } const action = increment(3) // { type: 'counter/increment', payload: 3 } /* 使用 createAction 後可以更加省略 */ import { createAction } from 'redux-toolkit' const INCREMENT = 'counter/increment' const increment = createAction(INCREMENT) console.log(increment) // ƒ actionCreator() { ... },得到 action creator function console.log(increment()) // {type: 'text/increment', payload: undefined},叫用其 action creator function 就會得到一開始傳進的 type const action = increment(3) console.log(action) // 叫用 action creator function 後就會得到完整的 action 物件 // { type: 'text/increment', payload: 3 } -> 帶入的 parameter 就會直接變成 payload 的 value const action2 = increment({ value: 3 }) console.log(action2) // 如果想要傳多個資料,就傳入物件 // { type: 'text/increment', payload: { value : 3 }} -> 但要注意的是之後還是會被 payload 包起來 ``` ### 底層被呼叫的 `createReducer` 更方便地建立 reducer function,提供 `Builder Callback` notation + 不同的 methods 建立相當於一般 redux 的 `swtich case` - 當然也可以使用一般的 `Map Object` notation,但是 `Map Object` notation 只能用在 JavaScript,無法使用 TypeScript,所以還是建議使用 `Build Callback` notation 的方式建立 reducer function 內的 cases ```javascript /* 透過 createAction 更方便地建立 actions / actionCreator */ const increment = createAction('counter/increment') const decrement = createAction('counter/decrement') const incrementByAmount = createAction('counter/incrementByAmount') /* 雖然 Map Object notation 看起來比較直覺,但是官方還是建議使用 Builder Callback nation 建立 cases */ const counterReducer = createReducer(0, { increment: (state, action) => state + 1, decrement: (state, action) => state - 1, incrementByAmount: (state, action) => state += action.payload }) ``` - `createReducer` 有使用 Immer 套件,因此可以用 immutable 的方式更改 state(因為最後套件會改成 mutable 的寫法安全地更新 state) 原本單純使用 redux 的寫法: ```javascript const initialState = { value: 0 } function counterReducer(state = initialState, action) { /* switch case -> reducer 收到符合的 action 才會執行事先定義好的任務 */ switch (action.type) { case 'increment': return { ...state, value: state.value + 1 } case 'decrement': return { ...state, value: state.value - 1 } case 'incrementByAmount': return { ...state, value: state.value + action.payload } default: return state } } ``` 使用 `createReducer` 簡化建立 reducer function 的步驟: ```javascript import { createAction, createReducer } from '@reduxjs/toolkit' interface CounterState { value: number } /* 透過 createAction 更方便地建立 actions / actionCreator */ const increment = createAction('counter/increment') const decrement = createAction('counter/decrement') const incrementByAmount = createAction('counter/incrementByAmount') const initialState = { value: 0 } const counterReducer = createReducer(initialState, (builder) => { /* 透過 builder callback + 方法建立相當與一般 redux 的 switch case */ builder .addCase(increment, (state, action) => { state.value++ }) .addCase(decrement, (state, action) => { state.value-- }) .addCase(incrementByAmount, (state, action) => { state.value += action.payload }) }) ``` 實作範例: 1. 基本上一個 `slice` 就是一個 file,方便管理 ```javascript= // src/store/counter.js const initialState = { value: 0 } export const counterSlice = createSlice({ name: 'counter', // 這組資料的 name initialState, // 這組資料的起始值 /* * 1. reducers * 2. 會直接建立 action,所以 methods name = action name * 3. 有 immer 套件的幫忙,可以用 immutable 的寫法更改 state * */ reducers: { increment: (state) => ({ value: state.value + 1, }), decrement: (state) => ({ value: state.value - 1, }), }, }); export const { increment, decrement } = counterSlice.actions // export action creators export default counterSlice.reducers // export reducer functions 最後整合至根 reducer ``` ## `configureStore` -> 建立中樞 store ```javascript= // src/store/index.js import { configureStore } from '@reduxjs/toolkit' import counterReducer from './counter' export const store = configureStore({ /* * reducer -> rootReducer * - { name: childReducer } 放入 childrenReducer * */ reducer: { counter: counterReducer, }, }) export default store ``` ## 記得到專案的接口加上 `store` ```javascript= import React from 'react' import ReactDOM from 'react-dom' import './index.css' import App from './App' import { store } from './app/store' import { Provider } from 'react-redux' ReactDOM.render( // 其子層都可以取得 store 管理的 state <Provider store={store}> <App /> </Provider>, document.getElementById('root') ) ``` ## `Thunk` ![](https://hackmd.io/_uploads/Bk41Pxe2i.png) > `Thunk` 是一個函式回傳另一個函式時,那個被回傳出來的函式。基本上可以想成 HOF(high order function) 回傳出來的函式。 ```javascript // ref. https://daveceddia.com/what-is-a-thunk/ function wrapper_function() { // this one is a "thunk" because it defers work for later: return function thunk() { // it can be named, or anonymous console.log('do stuff now'); }; } wrapper_function()() // 'do stuff now' const a = wrapper_function() a() // 'do stuff now' ``` ### `Thunk` in React Toolkit 使用 React Toolkit 時,`dispatch()` 不僅是收 `action` 當作 `argument`,也可以傳入一個 HOF,這時 HOF 回傳的 `Thunk`,`dispatch()` 會幫忙叫用(所以開發者不需要手動叫用其函式),除此之外,回傳的 `Thunk` 還會自動有 `dispatch` 當作 parameter,讓開發者可以在 `Thunk` 內叫用 `dispatch(action)` 改變 store。 ```javascript= /* STEP 1: HOF 回傳 Thunk */ const test = (input) => { /* STEP 3: 這個函式會自動被叫用,並且自動擁有 dispatch as argument,可以直接在這個函式內操作 dispatch */ return async(dispatch) => { try { const response = fetch(...) dispatch(successActionCreator(...)) } catch(error) { dispatch(errorActionCreator(...)) } } } /* STEP 2: dispatch HOF,dispatch() 發現是一個函式(Thunk)而不是 action object,就會幫忙叫用 Thunk */ store.dispatch(test('hello world')) ``` #### 這個好處是什麼? ![](https://hackmd.io/_uploads/HJBZCqy3s.png) Reducer function 必須是 pure function,因此無法在 Reducer function 內處理打 API 的相關事情,所以必須先執行完 API 的任務,再依照結果 `dispatch(action)` 更新 store 內的資料。 此時可以選擇在 1. component 執行打 API 的任務 + `dispatch(action)` 告知 store 要更新資料 2. 透過 React Toolkit,把任務 + `dispatch(action)` 抽離 component,component 只要負責「叫用」即可 - 這樣子可以讓 component 內不會有太多複雜的邏輯,component 單純「叫用」+ view 在 component 內處理執行打 API 的任務 + 後續處理: ```javascript= // in component // 省略其他不重要的部分,只留下處理打 API 行為的 useEffect /* 在 component 處理打 API 的邏輯 + 後續處理 */ useEffect(() => { const putRequestResultHandler = async () => { /* 外部 scope 有先建立是否初次載入的閘門 */ if (isFirstRender) { isFirstRender = false; return; } dispatch( toggleNotification({ notificationConfig: { status: "pending", title: "Waiting...", message: "Sending request...", }, }) ); try { await fetch( `${BASE_URL}${CART_END_POINT}`, getFetchConfig({ method: "PUT", body: cartInfo }) ); /* API 成功就... */ dispatch( toggleNotification({ notificationConfig: { status: "success", title: "Success!", message: "Send request successfully", }, }) ); } catch (e) { /* 失敗就... */ dispatch( toggleNotification({ notificationConfig: { status: "error", title: "Error!", message: "Send request failed", }, }) ); } }; putRequestResultHandler(); /* redux 會保證每次 re-render 拿到的 function 是同一個 */ }, [cartInfo, dispatch]); ``` 運用 `Thunk`,用 `dispatch(thunk)` 把打 API + 後續處理拉出 component: ```javascript= // in component useEffect(() => { if (isFirstRender) { isFirstRender = false; return; } /* component 只負負責「叫用」,就像是一般 `dispatch(action)` 告知 store 更新資料一樣,但這次 React Toolkit 會叫用 thunk */ dispatch(sendCartData(cartInfo)); /* redux 會保證每次 re-render 拿到的 function 是同一個 */ }, [cartInfo, dispatch]); ``` ```javascript= // in slice /* 打 API + 後續處理拉出來 */ export const sendCartData = (cart) => { /* 記得要 return 一個函式(Thun),屆時 React Toolkit 會幫忙叫用這個 Thunk 讓後續的任務可以繼續執行 */ return async (dispatch) => { dispatch( toggleNotification({ notificationConfig: { status: "pending", title: "Waiting...", message: "Sending request...", }, }) ); const sendRequest = async () => { /* fucntional scope 的緣故,所以可以拿到外層的 parameter */ const response = await fetch( `${BASE_URL}${CART_END_POINT}`, getFetchConfig({ method: "PUT", body: cart }) ); if (!response.ok) { throw Error("something went wrong"); } }; try { await sendRequest(); dispatch( toggleNotification({ notificationConfig: { status: "success", title: "Success!", message: "Send request successfully", }, }) ); } catch (error) { dispatch( toggleNotification({ notificationConfig: { status: "error", title: "Error!", message: "Send request failed", }, }) ); } }; }; ``` ## 除了自己寫 `Thunk` 之外,還可以用 React Toolkit 給的 method `createAsyncThunk()` ==可以參考課程助教寫的[範例](https://www.udemy.com/course/react-the-complete-guide-incl-redux/learn/lecture/25600382#questions/15002834)== > A function that accepts a Redux action type string and a callback function that should return a promise. ### `createAsyncThunk()` 也是回傳 `actionCreator()` > `actionCreator()` 是建立 `action` 物件 ![](https://hackmd.io/_uploads/ByzBV7e3o.png) ### 簡單範例 ```javascript= import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' /* * STEP 1: 透過 createAsyncThunk 建立 thunk * * 第一位 parameter 為 string,當作 type / identifier * 第二位 paameter 為 function,且該 function 必須回傳 Promise * */ const testThunk = createAsyncThunk('type', async () => { const response = await fetch(...) if (!response.ok) { throw Error('Something went wrong.') } return response.json() }) /* 當然也可以收外部資訊 */ const testThunkWithInput = createAsyncThunk('type2', async(someInput) => { const response = await fetch(...) if (!response.ok) { throw Error('Something went wrong.') } return response.json() }) const testSlice = createSlice({ name: 'testSlice', initialState: { ... }, reducers: { // standard reducer logic, with auto-generated action types per reducer }, /* * STEP 2: 在 slice.extraReducers 建立 reducer * * 1. createAsyncThunk 建立的 type 必須放在 extraReducers 中 * 2. 建議用 builder callback notation,在 TypeScript 中才可以有型別的支援 * 3. Redux Toolkit 非常厲害,會拿 Promise 的 status 當作執行任務的 key,開發者可以針對 Promise 的各種 status 寫任務 * */ extraReducers: (builder) => { /* STEP 3: 可以依照 Promise 的 status 定義要處理的任務 */ builder /* Promise 一開始發送時 */ .addCase(testThunk.pending, (state, action) => { // 一樣可以用 immutable way 更新 state // 也是用 action parameter 取得外部傳入的資訊 state.text = action.payload.text ... }) /* Promise 成功時(一般為打 API 成功)*/ .addCase(testThunk.fulfilled, (state, action) => { ... }) /* Promise 失敗時(一般為打 API 失敗)*/ .addCase(testThunk.rejected, (state, action) => { ... }) .addCase(testThunkWithInput.pending, (state, action) => { ... })... } }) /* 就和一般情境下在 component dispatch(action) 告知 store 要更新 state 一樣,所以這裡其實就是 dispatch(action) 的概念 */ dispatch(testThunk()) /* 也可以接收外部資訊(透過 function parameter) */ dispatch(testThunkWithInput(123)) ``` ### 與自己撰寫 `Thunk` 的思路不同 1. 自結撰寫 `Thunk`: - 需要自己手動處理非同步的流程(類似一條龍的概念) - 處理完流程後再 dispatch(action) -> action created by creator function -> creator function created by slice 2. 使用 `createAsyncThunk()`: - 流程被 React Toolkit 拆開了(並且被當作 reducer function) - Promise status as reducer type - 回歸到一般 reducer function(有 `state` 及 `action` 的 argument) - dispatch(action) -> action created by creator function -> creator function created by createAsyncThunk ## Recap - RTK 底層有用 immer 套件,因此可以用 immutable 的方式更改資料 - `createSlice()` 建立的是資料的群組,並且有呼叫 `createAction()` 及 `createReduer()` -> 更方便建立設定 - 與基本 React-Redux 的核心概念相同:==action 的 type 是呼叫對應的 reducer 表的 key== - actionCreator 是回傳 action 的函式 - `Thunk` 就是 HOF 被回傳出來的函式 - `useDipatch()` 收到 HOF 會自動叫用被回傳出來的 `Thunk`,該 `Thunk` 的 paramter 會有 `dispatch` 的存取權 - 透過 functional scope 就可以拿到外層函式 argument - `createAsyncThunk()` 其實也是回傳 actionCreator,所以 `dispatch(createAsyncThunk() -> action object)` - `createAsyncThunk()` 是用 `Promise status(pending, fulfilled, rejected)` 區分不同狀態的 `reducers` -> in React-Redux (different switch cases) ## 自己的小範例 https://codesandbox.io/s/react-toolkit-lian-xi-5l4453 ## 參考資料 1. [[Redux] Redux Toolkit (RTK) 筆記](https://pjchender.dev/react/redux-toolkit/) 2. [Redux Toolkit Quick Start](https://redux-toolkit.js.org/tutorials/quick-start) 3. [What the heck is a 'thunk'?](https://daveceddia.com/what-is-a-thunk/) 4. [React - The Complete Guide (incl Hooks, React Router, Redux) - sec. 19 Advanced Redux](https://www.udemy.com/course/react-the-complete-guide-incl-redux/)