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