# [React] Redux 筆記
###### tags: `React` `前端筆記` `Udemy 課程筆記`
## state(資料)按照使用情境可以大致區分成三類

### local state
只存在於 component 之內
### cross-component state
透過 `props` 傳遞的 state(於不同 components 之間使用)
### app-wide state
整個 app 都需要使用的 state
- 已登入使用者的資訊
- 主題顏色
- 是否需要開啟 modal 等等
基本上 Redux 就是用來解決 app-wide state 傳遞的麻煩(用 `props`,或者使用 `useContext`)
## Redux 是什麼?

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

### 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;
}
};
```
## 流程

以上圖為例子,實際上資料傳遞大概長這樣子:
### 初始化 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

## 多個 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)