# Diving into Redux - React compelte guide ###### tags: `Javascript, React` # Module Inro * 會介紹 Redux 這個 lib 是什麼 * 為何他這麼重要 * 以及如何使用它 ![](https://i.imgur.com/20Yl869.png) # Another Look At State In React Apps > Redux 是一個管理 state 的系統,主要應用在跨元件或是 app-wide 類型的 state ![](https://i.imgur.com/4wvLDcS.png) 來看看這三種 state 的差異 1. local state 主要 state 的發生以及改變在一個檔案中,例如監聽 user input 或是 toggle 按鈕讓文案開啟\ 關閉,主要在單一個元件中操作 useState, useReducer 2. cross-component state 跨元件使用 state ,舉例 overlay modal 這種,根據數個不同的元件來決定是否 modal 來執行,就會透過各個不同元件的 props 傳遞來達成 3. app-wide state 跟上面很像,但是更廣,舉例 authentication state 會根據登入狀態修改全站 UI ,一樣可以透過傳遞 props 來達成 ![](https://i.imgur.com/MaawAtU.png) 基於 2, 3 點要到處傳遞 props 相當麻煩,所以衍生出了 React Context API 以及現在要介紹的 Redux ,不過既然都有 Context API 為何 Redux 還會出現呢? # Redux vs React Context > 會有 Redux 的出現自然是因為 Context 有其缺點 ## 注意! **context API 跟 Redux 是可以同時出現的,Redux 控管整體的程式 state ,部分幾個 component 可以使用 context API 各別控制 state** ## 缺點 使用 context API 會因為有各種不同的 state 要傳遞,因此需要建立複數的 provider 來供應這樣的需求,就會變成這種情況,尤其是當 app 相當大的時候 ![](https://i.imgur.com/r4N7v8P.png) 不過這時候你會想,這樣把全部的 state 全部寫在同一個 provider 就好啦? 但會導致扣又肥又大,非常不好維護 ![](https://i.imgur.com/c9j0Qr6.png) 一篇來自 React 開發者對於 context API 的描述在於,對於 state 更新率高過一般的的情況下,其效能會比較差 ![](https://i.imgur.com/BRFVlOK.png) # How Redux Works > central data 不會直接被 component 改動,必須透過 action 告訴 Reducer component 想做什麼, Reducer 內處理好後在覆蓋掉 central data ,然後 central 就會告訴訂閱的 components 需要改動的 UI ![](https://i.imgur.com/JL6paTV.png) # Exploring The Core Redux Concepts > 這邊會簡單操作一個 app 每次使用 action 後會使 state +1 的程式,並且操作 node 來執行 ## 注意! reducer function 一定會是 pure function ![](https://i.imgur.com/BOOMdbn.png) pure function 代表: 放入一樣的 input ,輸出的的結果每一次都會是一樣的 output ,並且不會有 side effect 1. 建立 store 操作 createStore 並且帶入參數 reducer 2. 建立 reducer function,他會吃兩個參數: state, action 並且返回 state 來更新 store 3. 建立 counterSubscriber 這邊是在模擬 UI component 它會收到 store 的 state 來更新 UI 4. 將設置好的 subscriber 跟 store 連結使用 store 內的函式 subscribe 5. 定義 action 名稱並且使用 store 內的函式 dispatch 來帶入 ```javascript= const redux = require('redux'); // 範例使用 node 所以這樣來引入 redux const countReducer = (state = { counter: 0}, action) => { // 這邊 counter 要給預設值,設置為 0 return { counter:state.counter +1, }; // return +1,因此初始值為 1 }; const store = redux.createStore(countReducer);// 裡面的參數會是 reducer func const counterSubscriber = () => { const latestState = store.getState(); // 是 createStore 內建的函式,用途在於取得最新的 state console.log(latestState); // 這邊是 node 執行的內容 }; store.subscribe(counterSubscriber); store.dispatch({type: 'increment'}); // 這邊使用 dispatch 來執行特定 type 的動作 ``` 這邊印出直接 increment 後的內容也就是 2 !(畫面已經更新) ![](https://i.imgur.com/PSJ8Ipn.png) # More Redux Basics > 一般 reducer function 中都會處理複數情況的 action 其實也不複雜就是加上判斷式針對 type 去做辨別即可,如果都沒有符合的 action 則 return 原本的 state ```javascript= const countReducer = (state = { counter: 0}, action) => { if (action.type ==='increment'){ return { counter:state.counter +1, }; } else if( action.type === 'decrement') { return { counter:state.counter -1, }; } else { return state; } }; ``` # Preparing a new Project > 這邊要把上面的加減的 app 搬上去 react 操作 要特別注意的是在安裝 redux 時,如果是操作在 react 上要記得加上 react-redux `npm install redux react-redux` # Creating a Redux Store for React * 下方的扣可以看出跟 node 明顯的不同在於, import redux 的方式以及因為要在其他元件使用 store 所以必須 export 出去 * 另一點是不需要使用 subscribe 以及 dispatch 函式(因為牽涉到其他元件,不會只在這邊操作) 這樣子 store 的內容就建立好了 ```javascript= import { createStore } from 'redux'; const countReducer = (state = { counter: 0}, action) => { if (action.type ==='increment'){ return { counter:state.counter +1, }; } else if( action.type === 'decrement') { return { counter:state.counter -1, }; } else { return state; } }; const store = createStore(countReducer); export default store; ``` # Providing the Store > 這邊的動作跟 context API 非常像,使用 provider 包裹最外層的 app 來傳遞所有的 props 以及操作 action 並且要記得綁定 store 當作 props 最外層的 index.js ```javascript= import React from "react"; import ReactDOM from "react-dom"; import { Provider } from "react-redux"; import store from "./store/index"; import "./index.css"; import App from "./App"; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") ); ``` # Using Redux Data in React Components > 這邊要做的事情就是解釋 subscribe 在 react 中的行為 要把 store 中的內容呈現到 UI 剛剛解釋說得使用 subscribe ,但是使用 react 則是這樣操作: * import useSelector 用來選取一小部分需要的 state * 並且指派給變數後就可以操作那個 state 進去 jsx 中 如此一來這個 Counter.js 就跟 store subscribe 好,並且可以把 state 的數字呈現到頁面上摟 ![](https://i.imgur.com/AZ379ww.png) ```javascript= import classes from './Counter.module.css'; import { useSelector } from 'react-redux'; const Counter = () => { const toggleCounterHandler = () => {}; const counterValue = useSelector( state => state.counter); return ( <main className={classes.counter}> <h1>Redux Counter</h1> <div className={classes.value}>{counterValue}</div> <button onClick={toggleCounterHandler}>Toggle Counter</button> </main> ); }; export default Counter; ``` # Dispatching Actions From Inside Components > 這邊實際來操作 dispatch function * dispatch 並不會攜帶內容進來元件,但它需要執行並且指派給變數做操作 * 接下來就是帶入 onClick 指向的函式,並且輸入 type 名稱即可 * 最後寫進去 jsx 中的 onClick 即可,畢竟 UI 的部分就是點擊可以做到加減的動作 ```javascript= const dispatch = useDispatch(); const incrementHandler = () => { dispatch({type: 'increment'}); }; const decrementHandler = () => { dispatch({type: 'decrement'}); }; ``` # Redux with Class-based Components > 直接把上面的 FC 改寫成 class components * 首先引入 component 來使用 extends 創建 class Counter * render 的部分操作 JSX 跟 FC 部分是一樣的 * 使用到的 methods 操作方式如下 最重要的部分是: * connect 如何操作 connect 會帶入兩個參數並且他會再返回一個函式,它會直接執行,其參數為 class component,connect 的兩參數為:一是取得 store 的函式,另一為 dispatch 的函式 connect 也一樣會以 subscribe 的作用 * 取得 store 內的 props 的方法 基本上跟 FC 一樣寫入 state ,並且給予 key name 後帶入對應的 state 中的值 * 以及 dispatch 的寫法 dispatch 的部分則是先給予 key name 後,執行匿名函式在其中執行 dispatch **要特別注意 JSX 中的值都修改成使用 this 以及 props 的方式帶入, onClick 執行函式的部分則是要記得 bind this 上去確保其 this 的指向正確** ```javascript= import classes from "./Counter.module.css"; import { connect } from "react-redux"; import { Component } from 'react'; class Counter extends Component { incrementHandler () { this.props.increment(); } decrementHandler () { this.props.decrement(); } toggleCounterHandler () {} render () { return ( <main className={classes.counter}> <h1>Redux Counter</h1> <div className={classes.value}>{this.props.counter}</div> <div> <button onClick={this.incrementHandler.bind(this)}>Increment</button> <button onClick={this.decrementHandler.bind(this)}>Decrement</button> </div> <button onClick={this.toggleCounterHandler}>Toggle Counter</button> </main> ); } } const mapStateToProps = state => { return { counter: state.counter }; } const mapDispatchToProps = dispatch => { return { increment: () => dispatch({type: 'increment'}), // 這邊的寫法也要注意 decrement: () => dispatch({type: 'decrement'}), } } export default connect(mapStateToProps, mapDispatchToProps)(Counter); // 這邊是最特別的地方 ``` # Attaching Payloads to Actions > 這邊要解釋如何使用 payloads ,像是 action 的參數的概念 在 dispatch type 的後方加入 payload ,名字是可以自定義的,雖然這邊 payload 是寫死的,不過一般情況下,可以透過 useState, useRef 直接輸入就可動態的帶入使用者的 payload 摟! ```javascript= const incrementHandler = () => { dispatch({type: 'increment', amount: 10}); }; ``` 並且在 redux 中操作的時候,使用即可相當簡易 ```javascript= if (action.type ==='increment'){ return { counter: state.counter + action.amount}; } ``` ![](https://i.imgur.com/mFVz9yy.png) # Working with Multiple State Properties > 這邊會多操作一種 state 來演示複數的 state 的情況下,如何在 redux 中操作 > 操作 toggle counter 讓按鈕點擊後,隱藏數字,雖然這個 state 相當單純甚至可以使用 local state 操作就好,不過基於 demo 需求,所以把它操作成 global state 來作用在 redux 內 這邊演示 store.js 內的操作,當新的 state showCounter 出現的時候,因為 Reducer 操作的方式是將 action 的操作覆蓋掉原本的 state ,因此內部的操作都必須加上 showCounter 儘管該 action 沒有關聯 ```javascript= if (action.type === 'decrement') { return { counter: state.counter - 1, showCounter: state.showCounter // 儘管沒有關聯但是因為複數的 state ,這邊也需要加上 }; } if (action.type === 'toggle') { return { showCounter: !state.showCounter, // 儘管沒有關聯但是因為複數的 state ,這邊也需要加上 counter: state.counter }; } ``` 在 Counter.js 操作第二個 state 一樣必須使用 useSelector 來抓取 state 後,dispatch 操作即可 ```javascript= const show = useSelector((state) => state.showCounter const toggleCounterHandler = () => { dispatch({ type: 'toggle' }); }; // 記得 JSX 的部分必須這樣條件操作來開關數字 return ( {show && <div className={classes.value}>{counter}</div>} ) ``` 就可以 toggle 數字摟! ![](https://i.imgur.com/b58ETtz.png) # How To Work With Redux State Correctly > 影片開頭提到一個重點 redux 的 Reducer 最後一定會回傳一個 state 來取代目前最新的 state ,但是要非常注意,**它並不會 merge 原本的 state 它做的是取代**!!非常重要 第一個重點: **redux 的 Reducer 回傳的 state 它並不會 merge 原本的 state 它做的是取代!!** 舉個例子: 假設 Reducer 中的 increment 我們忘記加上去了 showCounter 會發生什麼事呢? ```javascript= if (action.type === 'increment') { return { counter: state.counter + 1, }; } ``` 它會覆蓋掉原來的 state ,變成 showCounter 永遠消失,因此 show 永遠都是 undefined 所以這樣的 side effect 就會讓數字從此不再出現 第二個重點: **千萬不要 mutate Reducer 內的 state** 因為 物件或是陣列是傳址的特性,容易發生問題而找不到地方解決! 傳址 (referrence type) 很容易造成,當你修改其屬性,因為地址相同導致,你改了內容其他地方的內容也跟著修改 例子: ```javascript= // 錯誤範例 if (action.type === 'increment') { state.counter ++ return state } // 錯誤範例 if (action.type === 'increment') { state.counter ++ return { counter: state.counter, showCounter: state.showCounter }; } // 正確做法 操作在 return 的物件內,讓原本的 state 保持原樣 if (action.type === 'increment') { return { counter: state.counter + 1, showCounter: state.showCounter }; } ``` [影片參考 - Reference vs Primitive Values](https://academind.com/tutorials/reference-vs-primitive-values) # Redux Challenges & Introducing Redux Toolkit > 接下來會介紹到 Redux Toolkit 來解決一些問題,不過會先介紹單純使用 Redux 的情況下並且程式使用的 state 用來越多,會遇到的挑戰 1. type 名稱不能打錯,不然絕對報錯 當然程式規模小不成問題,但是當多人協作,且規模擴大,這種情況就很可能發生 他是可以使用 JS 設定變數的方式來帶入,並且輸出到其他地方,不過下面會介紹更好的方式! 2. 剛剛有提到複數的 state ,每增加一個 steta 就必須所有的 type 都增加進去,程式才不會壞掉 這個情況當 state 越來越多,每一個 return 的 state 只會越來越長,似乎跟 Context API 一樣?不過其實他是有解的!待會再說 3. 還記得 Reducer 內的 state 不能 mutate 嗎? 但是當 state 愈來越多且複雜,有時候不是我們不想,是情況難以控制呀! 這邊 Redux toolkit 來救場啦! 他其實是 React redux 同一個團隊製造的工具,在下個章節中會開始介紹它! # Adding State Slices `npm i @reduxjs/toolkit` 先載個 toolkit ~ 引入 createSlice 後來操作 1. 首先他會需要一個 name 1. 再來使用初始值 1. 接下來輸入 reducers ```javascript= const counterSlice = createSlice({ name:'counter', initialState, reducers: { increment(state) { state.counter++; }, decrement(state) { state.counter--; }, increase (state, action) { state.counter = state.counter + action.amount; }, toggleCounter(state) { state.showCounter = !state.showCounter; } } }) ``` 跟 redux 最大的差異在於: * 不需要根據 type 去寫 if/else * 可以 mutate state ,但是其實在底層有做處理實際上 state 還是沒有被 mutate * 可以分多個資料夾去處理不同類別的 state # Connecting Redux Toolkit State > 這邊我們會操作 configureStore 來取代 createStore 它的功能在於一樣會創造一個 store 之外,並且結合多個不同的 slice 的 reducers 變成一個 reducer 會負責 global state ```javascript= const store = configureStore({ reducer: {counter: counterSlice.reducer} // 這邊如果有複數的 reducers 就可以在這個物件內新增下去,在底層的機制還是會把內部的 reducers 合併成一個輸出出去 }); ``` # Migrating Everything To Redux Toolkit 來看看整個 refactor 過後的扣,簡潔很多 action 的部分會輸出到需要使用的資料夾即可使用 ```javascript= import { createSlice, configureStore } from '@reduxjs/toolkit'; const initialState = { counter: 0, showCounter: true }; const counterSlice = createSlice({ name:'counter', initialState, reducers: { increment(state) { state.counter++; }, decrement(state) { state.counter--; }, increase (state, action) { state.counter = state.counter + action.payload; // 這邊的 payload 會是其預設值,不能向 redux 一樣可以自訂 }, toggleCounter(state) { state.showCounter = !state.showCounter; } } }) const store = configureStore({ reducer: counterSlice.reducer // 這邊要注意他的 reducer 不加 s }); export const counterAction = counterSlice.actions; export default store; ``` 使用 actions 的方式,就直接取用 counterAction 底下的 methods 即可,帶入的參數會以 payload 的方式被帶入 reducer 內使用 ```javascript= const increaseHandler = () => { dispatch(counterAction.increase(10)); }; ``` # Working with Multiple Slices > 剛剛學到的都是只有單一條 slice 且 state 很少的情況下,那如果有多條 slices 呢? 這邊會用一個登入狀態來解釋 global 的 state 如何呈現: * 比方說那個 log out 按鈕只有在登入狀態下才會出現 ![](https://i.imgur.com/JaOIXyR.png) 其實步驟目前看起來跟只有 counterSlice 差不多 重點差異在於: configureStore 內的因為多了一個 slice 它的寫法不太一樣了,變成需要有一個辨別 slice 的 identifier 在前面辨別,這邊要特別注意,**所以在 useSelector 的地方 state 的抓取會多一層** 這邊以 Counter 為例子,可以看到有兩層才能擷取到 state ![](https://i.imgur.com/ntIsFFW.png) ```javascript= const initialAuthState = { isAuthenticated: false }; const authSlice = createSlice({ name: 'authentication', initialState: initialAuthState, reducers: { login(state) { state.isAuthenticated = true; }, logout(state) { state.isAuthenticated = false; } } }) const store = configureStore({ reducer: { counter: counterSlice.reducer, auth: authSlice.reducer } }); export const counterAction = counterSlice.actions; export const authAction = authSlice.actions; ``` # Reading & Dispatching From A New Slice > 這邊會根據這個 login 的 state 去做到條件篩選 UI 以 App.js 舉例 * 首先抓出 isAuthenticated 來辦別登入與否 * 接下來在 JSX 中條件篩選 UI 來呈現 未登入 ![](https://i.imgur.com/051Hh28.png) 已登入 ![](https://i.imgur.com/y6Lng0C.png) App.js ```javascript= function App() { const isAuth = useSelector(state => state.auth.isAuthenticated); return ( <> <Header/> {!isAuth && <Auth/>} // 如果沒登入就顯示輸入帳號欄位,登入就讓其消失 {isAuth && <UserProfile/>} // 如果登入就顯示 user 帳號內容,沒有則消失 <Counter /> </> ); } ``` dispatch 的部分則是操作跟之前的一樣,引入 authAction 之後就可以使用它內部寫好的方法摟! Header.js ```javascript= const Header = () => { const isAuth = useSelector((state) => state.auth.isAuthenticated); const dispatch = useDispatch(); const logOutHandler = () => { dispatch(authAction.logout()); } return (...省略) } ``` # Splitting Our Code > 隨著 slices 越來越多 store.js 檔案越來越大,因此我們來拆分他們 原本都放在 store/index.js 內,這邊把兩種 slices 拆分成 auth, counter ![](https://i.imgur.com/JNiXNhx.png) 拆完之後的扣非常乾淨! store/index.js ```javascript= import { configureStore } from '@reduxjs/toolkit'; import counterReducer from './counter'; import authReducer from './auth'; const store = configureStore({ reducer: { counter: counterReducer, auth: authReducer } }); export default store; ``` slices 的部分使用 auth.js 做範例 * 基本上拆出 initialState * 以及整個 slice 的身體 * 記得操作 actions * 最後輸出 reducer 的部分(如果有使用到其他的部分也可以整個 authSlice 輸出 ```javascript= import { createSlice } from '@reduxjs/toolkit'; const initialAuthState = { isAuthenticated: false }; const authSlice = createSlice({ name: 'authentication', initialState: initialAuthState, reducers: { login(state) { state.isAuthenticated = true; }, logout(state) { state.isAuthenticated = false; } } }); export const authAction = authSlice.actions; export default authSlice.reducer; ```