# 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**

- **Redux**

**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>
);
```