---
# System prepended metadata

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

---

# [React] Redux 筆記

###### tags: `React` `前端筆記` `Udemy 課程筆記`

## state（資料）按照使用情境可以大致區分成三類

![](https://hackmd.io/_uploads/rkAxfLKKi.png)

### local state

只存在於 component 之內

### cross-component state

透過 `props` 傳遞的 state（於不同 components 之間使用）

### app-wide state

整個 app 都需要使用的 state

- 已登入使用者的資訊
- 主題顏色
- 是否需要開啟 modal 等等

基本上 Redux 就是用來解決 app-wide state 傳遞的麻煩（用 `props`，或者使用 `useContext`）

## Redux 是什麼？

![](https://hackmd.io/_uploads/rk_Hm8tFs.png)

> 數據機（store）用來保存 state（資料）的地方

Redux 是一個 app-wide 數據機（store），讓整個專案的 component 都讀取（或者變更）數據機裡面的資料，打破一般由上至下透過 `props` 傳遞資料的手段，當專案複雜時很方便，就不用瘋狂用 `props` 傳遞，造成傳遞過程過於繁瑣。


![](https://hackmd.io/_uploads/SkVvf4gqs.png)

### Redux 只會有一個 store

- 使用 Redux 的話一個專案只會有一個中樞 store
- 也許會有多個 reducer functions（每個 reducer functions 都是一組 state）

### Subscription

- component(view) 透過訂閱 store 可以取得保存在 store 內的 state
- 訂閱後，當被訂閱的 state 有更動時，其訂閱的 component 也會 re-render（達到 UI 刷新）
- 當 component 被銷毀時，React 會自動刪除訂閱

### Dispatch

component 發送 `action`，告知 redux 要對 state 做什麼操作

### Action

- 一個物件，通常會是 `{ type: 每個行為的代號, payload: 外部傳遞的資料 }`
- 用來描述要對 state 做出什麼操作（手動呼叫 reducer function 改變 `state` 的手段）

```javascript=
// 透過 type 告知這筆 action 要做什麼行為，並透過 payload 傳遞資訊
dispatch({ type: 'INCREASE', payload: { num: 100 } })
```

### Reducer Function

- `useReducer` 有參考 redux 的 reducer function 至概念
- ==負責變更 state 的函式，會先定義出所有 `action` 的種類（這樣子收到 `action` 才知道該怎麼做）==
- reducer function 有兩個 parameter `(state, action)`
   - `state` 當前的 `state`，供使用者拿來拷貝修改（==immutable==）
   - `action` 讀取 `dispatch(action)` 得知該做什麼
- 需遵照 immutable 的規則，所以無法直接改動現有的 state

```javascript=
/* 
 immutable 的修改範例
   1. 基本上就是必須先拷貝一份，再修改拷貝的資料
   2. 因為 object in js 是 mutable 的，所以透過 spread operator 淺拷貝一份修改
*/

const obj = {
  a: {
    c: 3
  },
  b: 100
}

const obj2 = {
  ...obj, // 淺拷貝 obj
  // 修改 obj.a
  a: {
    ...obj.a, // 淺拷貝 obj.a 的資料，開發者再寫入其他修改的資料
    e: 1000
  }
}
```


- 必須是 pure function，且每次回傳的結果就是新的 state
- 初始化 store 時就會先叫用 reducer functions
- 改變 state 的唯一手段
   - 基本上只會有兩種情況
      - 1. 此次的 `action` 是 reducer function 需要的，所以就會淺拷貝當前的 `state`，並改變拷貝的 `state` 修改完後 return，供 rootReduder 組新的 `state`
      - 2. 此次的 `action` 不重要，因此單純回傳當前的 `state`

```javascript=
// reducer 必須是 pure function，且「不能」直接改變當前 state，需要採用 immutable 的方式處理 object
const counterReducerFunc = (state = INIT_COUNTER, action = INIT_ACTION) => {
  // 初始化 store 時就會叫用 reducerFunc
  const { type, payload: { value } = {} } = action;

  switch (type) {
    // 當從 action 內收到 { type: 'INCREASE' } 就會執行 ...
    case 'INCREASE':
      return {
        ...state,
        counter: (state.counter += 1),
      };

    // 當從 action 內收到 { type: 'DECREASE' } 就會執行 ...
    case 'DECREASE':
      return {
        ...state,
        counter: (state.counter -= 1),
      };

    // 當從 action 內收到 { type: 'INCREASE_SPECIAL' } 就會執行 ...
    case 'INCREASE_SPECIAL':
      return {
        ...state,
        // 從 payload 取得 action 給的其他資訊
        counter: (state.counter += value),
      };

    // 當從 action 內收到 { type: 'TOGGLE_SHOW' } 就會執行 ...
    case 'TOGGLE_SHOW':
      return {
        ...state,
        isShow: !state.isShow,
      };

    // 此次的 action 不重要，因此回傳當前 state 即可
    default:
      return state;
  }
};

```

## 流程

![](https://hackmd.io/_uploads/HJxj9Lxqo.png)

以上圖為例子，實際上資料傳遞大概長這樣子：

### 初始化 store

- 建立 store 時就需要給 rootReducer
    - rootReducer 可以是由單個 reducer 或者是多個 reducers 組成
- 初始化 store 時會叫用 reducer（以便建立 state），所以必須一開始先建立初始值
- 每一個 reducer 就是一區 state（可以依照自己及專案的需求區分 state 群組）

```javascript=
// src/store/counter

// 目前 redux team 推薦使用 redux-toolkit 所以要使用 createStore 時會出現 warning，必須使用 legacy_createStore
import { legacy_createStore as createStore } from "redux";

const INIT_COUNTER = {
  counter: 0,
  isActive: false
};

/* 
  建立 store 時就會呼叫 reducer，所以一開始就必須給預設值，
  讓第一次呼叫 reducer 時可以正確地依照預設值建立 state
 */
const counterReducer = (state = INIT_COUNTER, action) => {
  console.log("called counterReducer");

  const { type } = action;

  switch (type) {
    case "INCREASE":
      console.log("++");
      return { counter: (state.counter += 1) };

    case "DECREASE":
      return { counter: (state.counter -= 1) };

    default:
      return state;
  }
};

export default counterReducer;
```

```javascript=
// 專案 store 的中樞
// src/store/index.js

import { createStore } from "redux";
import counterReducer from "./counter";

// store 需要一個 rootStore，而 rootStore 可以是一個或者多個 reducer functions 組成
const store = createStore(counterReducer);

export default store;

```

### 將專案綁定 store

需進入專案接口將專案及 store 綁定（這樣子 components 才可以取得 / 變更 store）：

```javascript=
// src/index.js

import { createRoot } from "react-dom/client";

import { Provider } from "react-redux";

import store from "./store";

import App from "./App";

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

/* 
  與 useContext 道理一樣，需要到專案主要街口設置 store，
  這樣子在 Provider 內層的所有 components 都可以取得 store / dispatch action 更改 store
*/

root.render(
  <Provider store={store}>
    <App />
  </Provider>
);
```

### component(view) 訂閱 store（取得 及 變更 state）

- `useSelector` 取得 store + 自動訂閱 
   - component 會依照 store 內的 state 顯示 view
   - 會自動訂閱（subscribe）store，如果 state 不一樣就會重新 re-render
- `useDispatch` 發送 action 
   - 可以建立專門產生 `action` 的函式，避免重複寫相同的 `typeName` 及打錯字

```javascript=
// store/dispatches

export const increaseAction = () => ({ type: 'INCREASE' })
```

```javascript=
// src/components/Child

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

import { increaseAction, decreaseAction } from "../store/dispatches";

const Child = () => {
  const counter = useSelector((store) => store.counter);
  const dispatch = useDispatch();

  const incraseHandler = () => {
    /* 
      可以把 action 變成一個函式（該函式就專門建立 action），
      可以避免打錯字 + 一直重複打相同的 typeName
    */
    dispatch(increaseAction());
    // dispatch({ type: "INCREASE" });
  };

  const decreaseHandler = () => {
    dispatch(decreaseAction());
  };

  return (
    <div>
      <h1>我是 Child component</h1>
      <p>
        當前 counter: <span>{counter}</span>
      </p>
      <div>
        <button onClick={incraseHandler}>increase</button>
        <button onClick={decreaseHandler}>decrease</button>
      </div>
    </div>
  );
};

export default Child;

```

### 透過 component 發送 dispatch，告知 store 要更改 store 

> dispatch(action) -> reducer -> store -> views get new state

#### STEP 1: 使用者透過 view 發送更改 state 的需求

- `dispatch(action)` 綁定事件，所以使用者點擊（觸發事件），便會呼叫 `dispatch(action)`

```javascript=
dispatch({ type: 'INCREASE' })
```

#### STEP 2: store 會把 action 給 rootReducer，而 rootReducer 會再把 action 傳給其巢狀的 children reducer functions

- 其巢狀 children reducer functions 都會被叫用（並且都可讀取一開始被發送的 `action`）

#### STEP 3: reducer function 依照收到的 `action` 執行變更 state 的任務

- reducer function 第一位為當前的 state，第二位為接收的 `action`
- redux 確保開發者可以藉由第一位拿到當前 render 的 state，所以可以把它拿來拷貝變更 state
- store 內的 rootReducer 會掌握所有 children reducer functions 回傳的 state，並統整成一個單一的巨型 state

```javascript=
// src/store/counter.js

// ...

const INIT_COUNTER = {
  counter: 0,
  isActive: false
};

/* 
   初始化後第一位 state 就是當前 render 是的 state，可以拿來拷貝更新 state
 * */
const counterReducer = (state = INIT_COUNTER, action) => {
  const { type } = action;

  switch (type) {
    case "INCREASE":
      // 拿當前 state 更新 state，並且 return 新的 state
      return { counter: (state.counter += 1) };

    case "DECREASE":
      return { counter: (state.counter -= 1) };

    default:
      return state;
  }
};

// ...
```

#### STEP4 : 訂閱的 component 會收到新的 state，並更新頁面

- 訂閱 store 的 component 會比對 state 是否有更新，如果有的話就會重新 re-render component

![](https://redux.js.org/assets/images/ReduxDataFlowDiagram-49fa8c3968371d9ef6f2a1486bd40a26.gif)


## 多個 reducers 的管理

- `combineReducers` -> redux 提供的 method，讓開發者可以把多個 reducers 集成一個 map
- `createStore(combineReducers)` -> 建立 store
- 在 component 內訂閱時需要多給 `mapName`

```javascript=
// 管理 store 的接口
// src/store/index.js

import { createStore, combineReducers } from "redux";
import counterReducer from "./counter";
import descriptionReducer from "./description";

/* 
  1. 有多個 state group 的話就要使用 combineReducers({ reducersMap })
  將多個 reducers 變成一個 MAP 
  2. 然後在 component 內訂閱時要多給一層 -> store.mapName.stateName
*/
const rootReducer = combineReducers({
  counter: counterReducer,
  description: descriptionReducer
});

// 最後再丟進 createStore
const store = createStore(rootReducer);

export default store;

```

```javascript=
// src/components/Child

// ...

const Child = () => {
  /* 
   * 有在 store 的接口用 combineReducers 建立 reducerMap，所以在 component 訂閱時要多給一層 mapName 才可以正確取得訂閱目標
   * */
  const counter = useSelector((store) => store.counter.counter);
 

  // ...
  
  return UI ...
};

// ...

```

## 程式範例

https://codesandbox.io/s/udemy-react-sec18-redux-lggmdr

## Recap

1. state 有三種：local state, cross-component state and app-wide state
2. redux 處理的就是 app-wide state
3. redux 的資料處理是單向的：action -> reducers -> store -> new state
4. reducer function 是 state group，看自己或者專案需求決定 state group 的區分
5. reducer function 必須是 immutable，透過 parameters `(state, action)` 取得當前 state 及外部傳入的資訊


## 參考資料

1. [Day14-Redux 篇-介紹 Redux](https://ithelp.ithome.com.tw/articles/10273805)
2. [[Redux] Redux Basic 基礎](https://pjchender.dev/react/redux-basic/)
3. [React - The Complete Guide (incl Hooks, React Router, Redux) Sec. 18：Diving into Redux (An Alternative To The Context APl)](https://www.udemy.com/course/react-the-complete-guide-incl-redux/)
4. [[译]图解Redux - A cartoon Intro to Redux](https://www.jianshu.com/p/85086337645d)
5. [Redux Fundamentals](https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow#redux-application-data-flow)

