# Redux 筆記 (Redux Toolkit, createAsyncThunk, reselect) 此筆記是基於 zerotomastery.io 推出 React 課程 [Complete React Developer in 2022](https://www.udemy.com/course/complete-react-developer-zero-to-mastery/) 所撰寫。 ## 注意!此章節實作 Redux 方式已過時,現官方推薦使用 Redux toolkit 來建立 Redux!Redux toolkit 在文末介紹。 ## 149.&150. Redux vs Context Redux 與 Context 主要的不同之處有兩點: - 元件取得存放在 Context 或 Redux 的資料方式不同 - **Context** 若一元件要取得 Context 中儲存的資料,該元件必須被 `<Context.Provider>` 標籤包覆。`<Context.Provider>` 可作為該元件的父層,或者將整個 `App.js` 包住,讓整個 App 都能取用這個 Context。 - **Redux** 要取得 Redux store 的資料則只能用 Redux 提供之 `<Provider>` 標籤統一將整個 App.js 包住。 - 資料流 (Redux reducer 和 useRudeucer 使用方式不太一樣) - **Context** ![](https://i.imgur.com/914Pum9.png) - **Redux** ![](https://i.imgur.com/BBMHFkD.png) **Redux 中的 reducer functions 要匯集成一個 root reducer funcion,並且每個元件都是呼叫同一個 `dispach()`,一次跑所有 reducer functions 再一次把所有 state 傳回去各 components。然而,useRuducer 的 dispatch 都是各自對應不同的 reducer functions。** ## 151. React-Redux: Installation & 153. React-Redux: Creating User Reducer ### 安裝 Redux `yarn add redux react-redux redux-logger` - `redux-logger` 是一個紀錄 redux 運作的套件,幫助我們更清楚 Redux 流程,非必要安裝套件。 ### Redux 前置 code + useDispatch 1. 建立 src/store/root-reducer.js ```jsx import {combineReducers} from 'redux' export const rootReducer = combineReducers({ // reducers' key and value }) ``` 2. src/store 下依據不同 reducer 創建資料夾及檔案並且 export,這邊以 user.reducer.js 為例 ```jsx // src/store/user/user.reducer.js export const USER_ACTION_TYPE = { SET_CURRENT_USER: 'SET_CURRENT_USER', }; const INITIAL_STATE = { currentUser: null, }; // 因 redux 不使用 useReducer 故 state 初始值直接以 ES6 預設值方式給值 export const userReducer = (state = INITIAL_STATE, action) => { const { type, payload } = action; switch (type) { case USER_ACTION_TYPE.SET_CURRENT_USER: return { ...state, currentUser: payload, }; // 因 redux 中的所有 reducers 都會接收同一個 action(只有一個 dispatch) // 其它與此 action 無關的 reducers 的 switch default case 會直接回傳原本的 stat // 這個 reducer 就不會更新 state(記憶體位址仍相同) default: return state; } }; ``` 3. `root-reducer.js` 引入 `user.reducer.js` ```jsx import { combineReducers } from 'redux'; import { userReducer } from '../store/user/user.reducer'; export const rootReducer = combineReducers({ // reducers: key and value user: userReducer, }); ``` 4. 建立 `src/store/store.js` ```jsx import { compose, createStore, applyMiddleware } from 'redux'; import logger from 'redux-logger'; import { rootReducer } from './root-reducer'; // 中介函式會在 reducers 接收到 action 時先觸發 const middleWares = [logger]; const composedEnhancers = compose(applyMiddleware(...middleWares)); export const store = createStore(rootReducer, undefined, composedEnhancers); ``` 5. 在 `index.js` `import { Provider } from 'react-redux';` 並用 `<Provider>` 把整個 `<App>` 包住,再 `import { store } from './store/store';`,將 store 作為 props 傳給 `<Provider>` ```jsx import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import { BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; import { store } from './store/store'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <Provider store={store}> <BrowserRouter> <App /> </BrowserRouter> </Provider> </React.StrictMode> ); ``` - 邏輯重構: - 本來 `user.context.jsx` 中的 `onAuthStateChangedListener`、`createUserDocumentFromAuth` Firebase 身分驗證邏輯搬移至 App.js,因身分驗證是 top-level 重要。 - 將有參考到 `user.context.jsx` 的部分刪除。 - `setCurrentUser` 此時是一個發送 dispatch action 的 function,這邊另建 src/store/user/user.action.js 存放 `setCurrentUser` 但功能只剩 createAction 並沒有 dispatch 了,所以我們要改用 `import { useDispatch } from 'react-redux';` ```jsx import { Routes, Route } from 'react-router-dom'; import Home from './routes/home/home.component'; import Navigation from './routes/navigation/navigation.component'; import Authentication from './routes/authentication/authentication.component'; import Shop from './routes/shop/shop.component'; import Checkout from './routes/checkout/checkout.component'; import { useEffect } from 'react'; import { onAuthStateChangedListener, createUserDocumentFromAuth, } from './utils/firebase/firebase.utils'; import { setCurrentUser } from './store/user/user.action'; import { useDispatch } from 'react-redux'; const App = () => { const dispatch = useDispatch(); useEffect(() => { const unscribe = onAuthStateChangedListener((user) => { if (user) { createUserDocumentFromAuth(user); } dispatch(setCurrentUser(user)); }); return unscribe; }, []); return ( <Routes> // 略... </Routes> ); }; export default App; ``` ## 154. React-Redux: Selectors ### 取用 redux store 的資料 useSelector useSelector 接收一個 callback 作為參數,該 callback 的參數為 state 物件並且 return 你想選取的值。起初 createStore 時是以 rootReducer 作為參數,rootReducer 又匯集了不同 reducers, state 其實就是一個物件包含了不同的 reducers,我們就從這些 reducers 裡面將值提取出來。 **語法:** ```jsx const variable = useSelector((state) ⇒ state.reducer.值) ``` **範例:** ```jsx const currentUser = useSelector((state) => state.user.currentUser); ``` **本小節進度:** 將 `navigation.component.jsx` 中的 currentUser 從原本 Context 提取的方式,改為從 Redux store 提取。 ```jsx import { useSelector } from 'react-redux'; const Naigation = () => { const currentUser = useSelector((state) => state.user.currentUser); return ( <Fragment> // 略... </Fragment> ); }; export default Naigation; ``` **優化** useSelector 接收的 callback 目的就是要返回我們所想要選取的值,這個 callback 可能常常會需要用到它,我們就把它做成一個模組。 `src/store/user/user.selector.js` ```jsx export const selectCurrentUser = (state) => state.user.currentUser; ``` ```jsx // in navigation.component.jsx const currentUser = useSelector(selectCurrentUser); ``` ## 155. Categories Reducer **本小節進度:** 將 `categories.context.jsx` 改為 Redux。 src/store/categories 新增 1. `categories.reducer.js` 建立 reducer function - `categories.reducer` 記得要加入到 root reducer 裡面 2. `categories.types.js` 建立 action type 常數物件(具體 action 動作全大寫) 3. `categories.action.js` 建立 action 物件 ⇒ 填入 action type 和 payload 為參數 4. `categories.selector.js` 建立 selector 取出 store 裡面 state 的值 ```jsx // categories.reducer.js import { CATEGORIES_ACTION_TYPES } from './categories.types'; const INITIAL_STATE = { categoriesMap: {}, }; export const userReducer = (state = INITIAL_STATE, action = {}) => { const { type, payload } = action; switch (type) { case CATEGORIES_ACTION_TYPES.SET_CATEGORIES_MAP: return { ...state, categoriesMap: payload, }; default: return state; } }; ``` ```jsx export const CATEGORIES_ACTION_TYPES = { SET_CATEGORIES_MAP: 'SET_CATEGORIES_MAP', }; ``` ```jsx // categories.action.js import { createAction } from '../../utils/reducer/reducer.utils'; import { CATEGORIES_ACTION_TYPES } from './categories.types'; export const setCategoriesMap = (categoriesMap) => createAction(CATEGORIES_ACTION_TYPES.SET_CATEGORIES_MAP, categoriesMap); ``` ## 156. Categories Selectors - 邏輯重構: - 原本在 `categories.context.jsx` 中的 useEffect 向 Firestore fetching categoreis 資料搬移至 `shop.component.jsx`,因 Shop 的 Route 都需要這些資料。 ```jsx const Shop = () => { const dispatch = useDispatch(); useEffect(() => { const getCategoriesMap = async () => { const categoriesMap = await getCategoriesAndDocuments(); dispatch(setCategoriesMap(categoriesMap)); }; getCategoriesMap(); }, []); return ( <Routes> <Route index element={<CategoriesPreview />}></Route> <Route path=":category" element={<Category />}></Route> </Routes> ); }; export default Shop; ``` - 將有參考到 `user.context.jsx` 的部分刪除。 **本小節進度:** 將 `categories-preview.component.jsx`、`category.component.jsx` 中的 categoriesMap 從原本 Context 提取的方式,改為從 Redux store 提取。 ```jsx export const selectCategoriesMap = (state) => state.categories.categoriesMap; ``` ```jsx import { selectCategoriesMap } from '../../store/categories/categories.selector'; const categoriesMap = useSelector(selectCategoriesMap); ``` ## 157. Business Logic in Our Selectors **本小節重點:** Redux reducer 中所存取的資料都應該要是最原始的格式,基本上就是透過 API 回傳的資料,然後使用 selector 將原始資料格式轉變為你所想要的樣子,你可以針對原始格式資料去建立多個 selector。拿到你想要的值。 **本小節進度:** - 原本 firebase.utils.js 中的 `getCategoriesAndDocuments()` fetching 資料後是一個陣列,我們又另加工使它回傳一個大物件,現在將加工部分移除,純粹回傳原始陣列。 - src/store/categories 下各檔案 code 進行修改,基本上是語意上的變數名稱調整,如 `SET_CATEGORIES_MAP` 改為 `SET_CATEGORIES`。 ## 158. What Triggers useSelector **本小節重點:** 因 redux 中的所有 reducers 都會接收同一個 action(只有一個 dispatch),其它與此 action 無關的 reducers 的 switch default case 會直接回傳原本的 state。然而,`combineReducers()` 收到更新時都會**回傳一個新的 state 物件,所以 useSelector 認為收到新的值就會 re-run。** ```jsx export const rootReducer = combineReducers({ user: userReducer, categories: categoriesReducer, }); ``` **結論** **只要呼叫了 `dispatch()`,當前路由下所有的元件中的 `useSelector()` 都會 re-run!(**儘管此 dispatch 只更新了特定一個 reducer 中的 state,其他 reducers 都 return 原本的 state) ## 160. Redux Triggers Extra Re-renders **本小節重點:** 承上,雖然 `useSelector()` 在呼叫 `dispatch()` 的時候會 re-run,但卻不一定會 re-render。這取決於 `useSelector()` 接收的參數(一個 callback 回傳 state.reducer.值),如果回傳值與前一次是相同的(其他 reducer 的 default case)就不會 re-render。但要注意的是你如何寫這個 callback,以下列程式碼為範例,這裡的 Array.reduce 永遠會回傳一個新的物件,所以總是會 re-render。 ```jsx export const selectCategoriesMap = (state) => state.categories.categories.reduce((acc, category) => { const { title, items } = category; acc[title.toLowerCase()] = items; return acc; }, {}); ``` 最理想的情況是當 `dispatch()` 呼叫時,全部的 useSelector re-run 但盡可能讓它們做最少事情,且 與確保本次 action 無關的 component 不會因為 useSelector re-render。要做到這件事可使用 Reselect Library 這個套件,我們下節介紹。 ## 162. Reselect Library **本小節重點:** Reselect 套件可以建立一個記憶化的 selector,它可以快取前一次 store state 存取的 reducers 以及底下的值,本次 dispatch 沒有更新到的 reducers 會回傳 default case (previous state),記憶化的 selector 發現 state.reducer 沒有改變(底下的值就是前一次 state)便不會再一次執行它的 callback 去做取值的動作,並繼續沿用前一次快取的值,**這可以確保 component 不會因為 useSelector 而 re-render**。 - 記憶化 selector 在元件初始 mount 時至少會 run 一次。 **安裝** `yarn add reselect` **使用範例 `categories.selector.js`** 1. 建立一個 getRedcerOrValueFromState function 其參數為 store state 並且回傳 state 下的其一 reducer。 ```jsx // 引入 createSelector import { createSelector } from 'reselect'; const selectCategoriesReducer = (state) => { console.log('selector 1 fired'); return state.categories; }; ``` 2. 使用 createSelector 建立記憶化 selector,語法如下: ```jsx createSelector( [getRedcerOrValueFromState function], (上面中括號變數的回傳值(可以是 reducer 或 reducer.值或記憶化 selector 的回傳值)) => { return getRedcerOrValueFromState function 回傳值.值; // return 你想取得的值 } ); ``` ```jsx export const selectCategories = createSelector( [selectCategoriesReducer], // selectCategoriesReducer 的回傳值是 state.categories (categoriesSlice) => { // 就是 state 底下的 categories reducer 即 categoriesSlice console.log('selector 2 fired'); return categoriesSlice.categories; // 得到存於 reuder 下的原始陣列 } ); ``` ```jsx export const selectCategoriesMap = createSelector( [selectCategories], // 這邊可以再放置一個記憶化 selector (categories) => { // selectCategories 的回傳值 console.log('selector 3 fired'); return categories.reduce((acc, category) => { const { title, items } = category; acc[title.toLowerCase()] = items; return acc; }, {}); } ); ``` **解析** 元件初始化 mount 時,分別會 `console.log` selector 1、2、3 ```jsx selector 1 fired selector 2 fired selector 3 fired ``` 當使用者登入/登出時,觸發了 Firebase 驗證 Listener 會呼叫 dispatch 傳入 setCurrent action ```jsx // in App.js const dispatch = useDispatch(); useEffect(() => { const unscribe = onAuthStateChangedListener((user) => { if (user) { createUserDocumentFromAuth(user); } dispatch(setCurrentUser(user)); }); return unscribe; }, []); ``` 這會讓所有 useSelector 被觸發,此 action 只會讓 categoriesReducer 回傳 default case (前一次 categoriesReducer 的 state), `categories.selector.js` 只會 `console.log` selector 1 fired,因為記憶化的 selector 發現 state.categories(這裡指整個 store state 的 categories reducer) 沒有改變(底下的值就是前一次 state,這裡指 state.categories.categories)便不會再一次執行它的 callback 去做取值的動作,並繼續沿用前一次快取的值,所以就不會執行 `console.log('selector 2 fired');`,更不用說 `console.log('selector 3 fired');`。 ```jsx import { createSelector } from 'reselect'; const selectCategoriesReducer = (state) => { console.log('selector 1 fired'); return state.categories; }; export const selectCategories = createSelector( [selectCategoriesReducer], (categoriesSlice) => { console.log('selector 2 fired'); return categoriesSlice.categories; } ); export const selectCategoriesMap = createSelector( [selectCategories], (categories) => { console.log('selector 3 fired'); return categories.reduce((acc, category) => { const { title, items } = category; acc[title.toLowerCase()] = items; return acc; }, {}); } ); ``` 而有用 useSeletor 去取用 selectCategoriesMap 也都不會因為這一次 Firebase 驗證的 dispatch 發生 re-render,因為記憶化元件會返回上一次快取的值。 ```jsx const categoriesMap = useSelector(selectCategoriesMap); ``` # 第 15 節: Redux Extended Tools ## 167. Redux-Persist 將 state 一併寫入 local storage (後面有更新 Redux Toolkit 寫法) **安裝** `yarn add redux-persist` **使用範例:** 1. `store.js` ```jsx import { compose, createStore, applyMiddleware } from 'redux'; import logger from 'redux-logger'; import { rootReducer } from './root-reducer'; // 1. import { persistStore, persistReducer } from 'redux-persist'; import { persistStore, persistReducer } from 'redux-persist'; // 2. import storage from 'redux-persist/lib/storage'; // 在任何瀏覽器中這個 storage 預設為 local stoage import storage from 'redux-persist/lib/storage'; // 3. 定義 persistConfig const persistConfig = { key: 'root', storage, blacklist: ['user'], // user will not be persisted whitelist: ['cart'], // only cart will be persisted }; // 4. 建立 persistedReducer const persistedReducer = persistReducer(persistConfig, rootReducer); const middleWares = [logger]; const composedEnhancers = compose(applyMiddleware(...middleWares)); export const store = createStore( // 5. 將 rootReducer 替換成 persistedReducer persistedReducer, undefined, composedEnhancers ); // 6. epxort persistor export const persistor = persistStore(store); ``` 2. `index.js` ```jsx import App from './App'; import { BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; // 1. import import { store, persistor } from './store/store'; import { PersistGate } from 'redux-persist/integration/react'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <Provider store={store}> // 2. <PersistGate> 放在 Provider 之下包住 App 並傳入 persistor <PersistGate persistor={persistor}> <BrowserRouter> <App /> </BrowserRouter> </PersistGate> </Provider> </React.StrictMode> ); ``` ## 168. Redux-Devtools Redux-Devtools 是一個 chrome 擴充功能用來追蹤所有 actions 行為並且會有一個控制面板可以用來顯示每個 action 發生的時間點。詳細使用方式可直接觀賞此小節影片。 **補充** logger 套件會追蹤所有 actions 行為並且 console.log 出來,我們可以將程式碼加入判斷式,當城市在非 production 模式 logger 才顯示。 ```jsx const middleWares = [logger]; // 改成下面程式碼 const middleWares = [process.env.NODE_ENV !== 'production' && logger].filter( Boolean ); ``` # 第 16 節: Asynchronous Redux: Redux-Thunk ## 169. Asynchronous Redux: Redux-Thunk 本小節內容節錄並改寫自 **[PJCHENder](https://pjchender.dev/react/redux-thunk/)。** > Redux-Thunk 是一個 middleware 讓你可以撰寫一個回傳 function 而非 action object 的 action creator,透過 thunk 你可以決定要如何發送 (dispatch) 一個 action,因此**適合用來處理非同步取得的資料狀態**,或者是在特定條件符合的情況下才發送。 > > 使用 redux-thunk 時,action creator 回傳的不是物件,而是一個帶有 `dispatch`、`getState`、`extraArgument` 參數(後兩個參數為 optional)的 **async function**,並在這個 async function 內會再依不同情況去呼叫 `dispatch(action obj)` ,而在元件中只是呼叫 `dispatch(thunk function creator)` 就好,將非同步操作全部轉移至 redux-thunk function 中。 > **安裝** `yarn add redux-thunk` **使用範例** 1. `store.js` ```jsx // import thunk 並放進 middleWares 陣列 import thunk from 'redux-thunk'; const middleWares = [ process.env.NODE_ENV !== 'production' && logger, thunk, ].filter(Boolean); const composedEnhancers = compose(applyMiddleware(...middleWares)); export const store = createStore( persistedReducer, undefined, composedEnhancers ); ``` 1. `categories.types.js` 因我們要將非同步請求搬到 redux 中操作,我們將本來一個 SET_CATEGORIES 拆成三個動作,非同步請求會先 START 並且可能會 SUCCEEDED 或 FAILED。 ```jsx export const CATEGORIES_ACTION_TYPES = { // SET_CATEGORIES: 'SET_CATEGORIES', FETCH_CATEGORIES_START: 'categories/FETCH_CATEGORIES_START', FETCH_CATEGORIES_SUCCEEDED: 'categories/FETCH_CATEGORIES_SUCCEEDED', FETCH_CATEGORIES_FAILED: 'categories/FETCH_CATEGORIES_FAILED', }; ``` 1. `categories.reducer.js` ```jsx import { CATEGORIES_ACTION_TYPES } from './categories.types'; // 1. 初始值新增 isLoadging、error const INITIAL_STATE = { categories: [], isLoading: false, error: null, }; export const categoriesReducer = (state = INITIAL_STATE, action = {}) => { const { type, payload } = action; // 2. 當 FETCH 開始 isLoading 就開始 switch (type) { case CATEGORIES_ACTION_TYPES.FETCH_CATEGORIES_START: return { ...state, isLoading: true, }; // FETCH 成功 payload 帶入取回的陣列、isLoading 關閉 case CATEGORIES_ACTION_TYPES.FETCH_CATEGORIES_SUCCEEDED: return { ...state, categories: payload, isLoading: false, }; // FETCH 失敗 payload 帶入 catch error 的值、isLoading 關閉 case CATEGORIES_ACTION_TYPES.FETCH_CATEGORIES_FAILED: return { ...state, error: payload, isLoading: false, }; default: return state; } }; ``` 1. `categories.action.js` 除了建立剛剛定義的三個 action 之外,要再額外建立一個 thunk async function creator,在 async function 裡頭決定如何 dispatch 其他三個 action。 ```jsx import { createAction } from '../../utils/reducer/reducer.utils'; import { CATEGORIES_ACTION_TYPES } from './categories.types'; import { getCategoriesAndDocuments } from '../../utils/firebase/firebase.utils'; // export const setCategories = (categoriesArray) => // createAction(CATEGORIES_ACTION_TYPES.SET_CATEGORIES, categoriesArray); export const fetchCategoriesStart = () => createAction(CATEGORIES_ACTION_TYPES.FETCH_CATEGORIES_START); export const fetchCategoriesSucceeded = (categoriesArray) => createAction( CATEGORIES_ACTION_TYPES.FETCH_CATEGORIES_SUCCEEDED, categoriesArray ); export const fetchCategoriesFailed = (error) => createAction(CATEGORIES_ACTION_TYPES.FETCH_CATEGORIES_SUCCEEDED, error); export const fetchCategoriesAsync = () => async (dispatch) => { dispatch(fetchCategoriesStart()); try { const categoriesArray = await getCategoriesAndDocuments('categories'); dispatch(fetchCategoriesSucceeded(categoriesArray)); } catch (error) { dispatch(fetchCategoriesFailed(error)); } }; ``` 1. `shop.component.jsx` 不再處理非同步行為 async/await 而是都交給 redux-thunk 處理 ```jsx import { Routes, Route } from 'react-router-dom'; import CategoriesPreview from '../categories-preview/categories-preview.component'; import Category from '../category/category.component'; import { useEffect } from 'react'; import { fetchCategoriesAsync } from '../../store/categories/categories.action'; import { useDispatch } from 'react-redux'; const Shop = () => { const dispatch = useDispatch(); /* useEffect(() => { const getCategoriesMap = async () => { const categoriesArray = await getCategoriesAndDocuments(); dispatch(setCategories(categoriesArray)); }; getCategoriesMap(); }, []); */ useEffect(() => { dispatch(fetchCategoriesAsync()); }, []); return ( <Routes> <Route index element={<CategoriesPreview />}></Route> <Route path=":category" element={<Category />}></Route> </Routes> ); }; export default Shop; ``` 補充 catch error 範例 `firebase.utils.js` ```jsx export const getCategoriesAndDocuments = async () => { const collectionRef = collection(db, 'categories'); const q = query(collectionRef); // reject error await Promise.reject(new Error('new error oops')) const querySnapShot = await getDocs(q); return querySnapShot.docs.map((docSnapShop) => docSnapShop.data()); }; ``` ## 171. Redux-Thunk Pt. 3 增加 loading 樣式 1. `src/components/spinner` 新增 spinner.component.jsx、spinner.styles.jsx 2. `categories.selector.js` 建立一個 selector 將 isLoading 的值取出 ```jsx export const selectCategoriesIsLoading = createSelector( [selectCategoriesReducer], (categoriesSlice) => categoriesSlice.isLoading ); ``` 1. `categories-preview.component.jsx` import `<Spinner>` 並在 isLoading 為 true 時顯示 ```jsx const CategoriesPreview = () => { const categoriesMap = useSelector(selectCategoriesMap); const isLoading = useSelector(selectCategoriesIsLoading); return ( <Fragment> {isLoading ? ( <Spinner /> ) : ( Object.keys(categoriesMap).map((title) => { const products = categoriesMap[title]; return ( <CategoryPreview key={title} title={title} products={products} /> ); }) )} </Fragment> ); }; ``` # Redux Toolkit 本節內容改寫並節錄自 **[Day16-Redux 篇-認識 Redux Toolkit](https://ithelp.ithome.com.tw/articles/10275089)、[PJCHENder](https://pjchender.dev/react/redux-toolkit/)。** > Redux Toolkit 是一個可以幫助你更有效率撰寫 Redux 的一個 library,它提供了一些 API 讓你更方便的建立 Store、Actions 和 Reducers。 > 以下為**本次實作練習有用到的幾個 API 或重點** **安裝** `npm install @reduxjs/toolkit react-redux` ## **configureStore** > 在 redux 中原本就有 `createStore` 這個方法,而 `configureStore` 可以視為加強版的 `createStore`,透過 `configureStore()` 可以簡化設定的流程、結合 slice reducers、添加和 Redux 有關的 middleware、並啟用 Redux DevTools 的擴充套件: > > - 內建 Redux DevTools Extension > - *內建 redux-thunk* 1. 建立 `src\store\store.js` ```jsx // 創建 store 要 import 的方法 import { configureStore } from '@reduxjs/toolkit'; import { combineReducers } from 'redux'; // import middlewares import logger from 'redux-logger'; import thunk from 'redux-thunk'; // 從各 slices 引入 reducers import { userReducer } from './user/user.slice'; import { cartReducer } from './cart/cart.slice'; import { categoriesReducer } from './categories/categories.slice'; const reducers = combineReducers({ user: userReducer, cart: cartReducer, categories: categoriesReducer, }); const middlewares = [ process.env.NODE_ENV !== 'production' && logger, thunk, ].filter(Boolean); export const store = configureStore({ // 也可以不用 combineReducers,直接將 reducers 寫在這裡就好 reducer: reducers, middleware: middlewares, }); ``` > 2. Provide the Redux Store to React (index.js) > > - 使用 `Provider` 將 App 包起來 > - 將剛剛透過 `configureStore` 建立的 `store` 帶入 `<Provider />` 中 ```jsx import App from './App'; import { BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; import { store } from './store/store'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <Provider store={store}> <BrowserRouter> <App /> </BrowserRouter> </Provider> </React.StrictMode> ); ``` ## **createSlice** > createSlice 能夠同時建立 action creator、reducers,管理起來更為方便。 在 `createSlice()` 這個函式中可以帶入 reducer function、slice name 和 initial state,將自動產生對應的 slice reducer,並包含對應的 action creators 和 action types 在內。在使用 createSlice 或 createReducer 撰寫 reducer 的時候可以不用再用 switch case 語法,它們的語法底層加入了 [immer](https://github.com/immerjs/immer),因此可以使用會有 side effect 的寫法去變更 state(**直接修改 state**),它背後會再幫你轉成 「immutable」的方式。 > ```jsx import { createSlice } from '@reduxjs/toolkit'; const initialState = { currentUser: null, number: 0, }; const userSlice = createSlice({ name: 'user', initialState, reducers: { // slice 會自動產生 action creators 和 action types setCurrentUser: (state, action) => { const { payload } = action; state.currentUser = payload; }, }, }); export const { setCurrentUser } = userSlice.actions; export const userReducer = userSlice.reducer; ``` `createSlice()` 產生的 `userSlice` 物件中有這些可用的屬性和方法: ```jsx userSlice.name; // user userSlice.actions.setCurrentUser.type; // "user/setCurrentUser" userSlice.reducer; // reducer // action creator userSlice.actions.setCurrentUser(); // {type:"user/setCurrentUser", payload: undefined} userSlice.actions.setCurrentUser(3); // {type:"user/setCurrentUser", payload: 3 } ``` ## createSelector ```jsx import { createSelector } from '@reduxjs/toolkit'; ``` > `createSelector` 這個工具來自 [Reselect](https://github.com/reduxjs/reselect) 這個套件。 > ## createAsyncThunk - createAsyncThunk 是一個接受 `Redux action type string` 和一個必須回傳 Promise 的 `payloadCreator ****callback` 的 function。 - createAsyncThunk 會基於你傳入的 action type 產生 Promise 生命週期並回傳一個 thunk action creator,呼叫後會執行你傳入的 callback 並且根據 Promise 的結果去 dispatch 不同 action。 > 簡化開發者要寫 `isLoading`、`error` 和 `data` 繁瑣的過程。 > ### **createAsyncThunk 參數** 1. `Redux action type string` 以 fetchCategoriesAsync 為例如下,將會產生 3 個 acions - pending: 'categories/fetchCategories/pending' - fulfilled: 'categories/fetchCategories/fulfilled' - rejected: 'categories/fetchCategories/rejected' 呼叫 `fetchCategoriesAsync()`,會先 dispatch `'categories/fetchCategories/pending'` 並根據第二個參數 return 的 Promise 結果去 dispatch `fulfilled` 或 `rejected`。 2. `payloadCreator ****callback` 接收 2 個參數如下,皆為 optional - `arg` 想傳入此 callback 的參數 - `thunk API` 一個物件包含了 dispatch、getState 方法等等 ### createAsyncThunk 實際使用範例 ```jsx import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import { getCategoriesAndDocuments } from '../../utils/firebase/firebase.utils'; const initialState = { categories: [], isLoading: false, error: null, }; export const fetchCategoriesAsync = createAsyncThunk( 'categories/fetchCategories', () => { // 必須回傳 Promise return getCategoriesAndDocuments('categories'); } ); const categoriesSlice = createSlice({ name: 'categories', initialState, extraReducers: (builder) => { builder .addCase(fetchCategoriesAsync.pending, (state) => { state.isLoading = true; }) .addCase(fetchCategoriesAsync.fulfilled, (state, { payload }) => { state.categories = payload; // Promise fulfilled 會成為 payload state.isLoading = false; }) .addCase(fetchCategoriesAsync.rejected, (state, { error }) => { const { message } = error; state.error = message; state.isLoading = false; }); }, }); export const categoriesReducer = categoriesSlice.reducer; ``` > 由於這些 action 並不是在 slice 中被定義,所以如果要在 createSlice 中監聽這些 action type,需要在 `extraReducers` 中透過 `builder.addCase` 來使用 > ### [extraReducers](https://redux-toolkit.js.org/api/createSlice#extrareducers) 不同的 Slice reducer 可以對同一個 action 做出獨立、個別的反應,而 extraReducers 允許 createSlice 去針對非此 createSlice 所創建的 actions 做出回應。 extraReducers 就是用來參照 「外部」的 actions,它們不會出現在 sliceObject.actions 中。 ## Redux-Persist 整合至 Redux Toolkit 1. `src\store\store.js` 調整 ```jsx import { configureStore } from '@reduxjs/toolkit'; import { combineReducers } from 'redux'; import logger from 'redux-logger'; import thunk from 'redux-thunk'; import { userReducer } from './user/user.slice'; import { cartReducer } from './cart/cart.slice'; import { categoriesReducer } from './categories/categories.slice'; // 1. import redux-persist&storage import { persistStore, persistReducer } from 'redux-persist'; import storage from 'redux-persist/lib/storage'; const reducers = combineReducers({ user: userReducer, cart: cartReducer, categories: categoriesReducer, }); // 2. persist config 設定 const persistConfig = { key: 'root', storage, blacklist: ['user'], whitelist: ['cart'], }; // 3. 建立 persistedReducer const persistedReducer = persistReducer(persistConfig, reducers); const middlewares = [ process.env.NODE_ENV !== 'production' && logger, thunk, ].filter(Boolean); export const store = configureStore({ // 4. configureStore reducer 替換成 persistedReducer reducer: persistedReducer, middleware: middlewares, }); // 5. export persistor export const persistor = persistStore(store); ``` 1. Provide the Redux Store to React (index.js) - 使用 `PersistGate` 將 App 包起來 - 將剛剛透過 `persistStore` 建立的 `persistor` 帶入 `<PersistGate />` 中 ```jsx import App from './App'; import { BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; import { store, persistor } from './store/store'; import { PersistGate } from 'redux-persist/integration/react'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <Provider store={store}> <PersistGate persistor={persistor}> <BrowserRouter> <App /> </BrowserRouter> </PersistGate> </Provider> </React.StrictMode> ); ```