# [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`

> `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'))
```
#### 這個好處是什麼?

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` 物件

### 簡單範例
```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/)