---
# System prepended metadata

title: 'Redux 筆記 (Redux Toolkit, createAsyncThunk, reselect)'

---

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