---
# System prepended metadata

title: '[React] Redux-Toolkit 筆記'
tags: [React, Udemy 課程筆記, 前端筆記]

---

# [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/)
