iT邦幫忙

2021 iThome 鐵人賽

DAY 15
1
Modern Web

用30天更加認識 React.js 這個好朋友系列 第 15

Day15-Redux 篇-實作範例

2022/08/03 補充

現在回來看發現自己以前寫的 Redux 還有很多要改進的地方,所以重構了一個範例,並使用了 custom hook useActions 搭配 redux 提供的 bindActionCreators 統一 export 多個 actions,這樣之後要 import 多個不同的 actions 時只要從 actionCreators.js 統一 import 時即可,更加方便。

使用 useActions 可以讓開發者不用重複的多寫例如 dispatch(actionName(某參數)) ... 的程式碼,只要寫 actionName(某參數) 即可,減少重複性的程式碼。

useActions:

import { useDispatch } from "react-redux";
import { bindActionCreators } from "redux";
import * as actionCreators from "../actionCreators";

export const useActions = () => {
  const dispatch = useDispatch();

  return bindActionCreators(actionCreators, dispatch);
};

重構後的範例

2022/01/26 補充

在範例中,可以看到 dispatch({ type: "increase", amount: 10 }); 等 action 物件,如果在多個地方使用的話就得重複去撰寫同樣的 action,所以可以建立一個 action creator 的函式,如果有多個地方同時去使用相同的 action,就只要去呼叫已經定義好 action 結構的該函式即可。

const increaseByNum = (num) => {
  return {
    type: "increase",
    amount: num
  };
};

<button onClick={() => dispatch(increaseByNum(10))}>Increase by 10</button>

2021/11/23 補充

這裡補充一個管理多個 Reducer 的範例:
程式碼範例


以下舊內容

在今天的文章中,我們將會使用 Redux 去完成一個計數器的範例程式。

第一步

使用 createStore 建立一個 Redux store ,並將 store 用 react-redux 的 Provider 元件提供給 React 的母元件 App

// store/index.js
import { createStore } from 'redux';

const counterReducer = (state = { counter: 0 }, action) => {
  if (action.type === 'increment') {
    return { counter: state.counter + 1 };
  }

  if (action.type === 'decrement') {
    return { counter: state.counter - 1 };
  }

  return state;
};

const store = createStore(counterReducer);

export default store;
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import App from './App';
import store from './store/index';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

第二步

在元件使用 useSelector 和 useDispatch 這兩個 hook ,這邊介紹一下這兩個 hook 的用法:

useSelector

使用這個 hook 可以從 store 裡面去拿取想要的 state 出來,效用大致等於 connect 元件的 mapStateToProps。

ex:
const counter = useSelector(state => state.counter);

特點:

  1. 可以返回各種型別的值
  2. useSelector 回傳的值(也就是取出的 state)若有更新,就會重新渲染元件
  3. 不接受 ownProps 參數,但可以透過 JavaScript 的閉包觀念取得元件中的 props

useDispatch

透過這個 hook 會回傳一個 dispatch 方法,可以讓我們 dispatch actions,用來取代 mapStateToDispatch。

const dispatch = useDispatch();

useStore

另外還有一個 hook useStore,不過我們暫時不會用到,但還是介紹一下,它可以取得 store,並進行 getState()、subscribe() 、dispatch() 等函式呼叫。

const store = useStore();

在程式碼中可以看到使用 useSelector 去取出 store 裡面的 counter state,並透過 useDispatch 回傳的 dispatch 方法去觸發 reducer。

// Counter.js
import { useSelector, useDispatch } from "react-redux";

const Counter = () => {
  const dispatch = useDispatch();
  const counter = useSelector((state) => state.counter);

  const incrementHandler = () => {
    dispatch({ type: "increment" });
  };

  const decrementHandler = () => {
    dispatch({ type: "decrement" });
  };

  return (
    <>
      <div>{counter}</div>
      <button onClick={incrementHandler}>Increment</button>
      <button onClick={decrementHandler}>Decrement</button>
    </>
  );
};

export default Counter;

完成以上程式碼的實作後,就可以點擊按鈕做加減了。

3. 增加多一點 state

接著我們多加一個 toggle 按鈕及一個點擊一次就增加多個數字的按鈕。

// store/index.js
import { createStore } from 'redux';

const initialState = { counter: 0, showCounter: true };

const counterReducer = (state = initialState, action) => {
  if (action.type === 'increment') {
    return {
      counter: state.counter + 1,
      showCounter: state.showCounter
    };
  }

  if (action.type === 'increase') {
    return {
      counter: state.counter + action.amount,
      showCounter: state.showCounter
    };
  }

  if (action.type === 'decrement') {
    return {
      counter: state.counter - 1,
      showCounter: state.showCounter
    };
  }

  if (action.type === 'toggle') {
    return {
      showCounter: !state.showCounter,
      counter: state.counter
    };
  }

  return state;
};

const store = createStore(counterReducer);

export default store;
// Counter.js
import { useSelector, useDispatch } from "react-redux";

const Counter = () => {
  const dispatch = useDispatch();
  const counter = useSelector((state) => state.counter);
  const show = useSelector((state) => state.showCounter);

  const incrementHandler = () => {
    dispatch({ type: "increment" });
  };

  const increaseHandler = () => {
    dispatch({ type: "increase", amount: 10 });
  };

  const decrementHandler = () => {
    dispatch({ type: "decrement" });
  };

  const toggleCounterHandler = () => {
    dispatch({ type: "toggle" });
  };

  return (
    <>
      {show && <div>{counter}</div>}
      <button onClick={incrementHandler}>Increment</button>
      <button onClick={increaseHandler}>Increase by 10</button>
      <button onClick={decrementHandler}>Decrement</button>
      <button onClick={toggleCounterHandler}>Toggle Counter</button>
    </>
  );
};

export default Counter;

到這邊實作就完成了!實作的程式碼範例在底下連結,明天將會介紹另一種撰寫 Redux 的 library: Redux toolkit。

程式碼範例(codesandbox)


2023/05/09 補充

在 Redux 官網看到一個更完整的範例 Redux Essentials, Part 6: Performance and Normalizing Data,這裡也補充給讀者參考,以下也介紹範例中的一些重要功能的使用。

1. createSelector 可以產生 memorized 的 selector,避免 useSelector 重新執行時回傳不一樣的 state 導致元件 re-render

這個在 Redux 官方文件 Memoizing Selector Functions 有提到,可以使用 createSelector,產生 memoized 的 selector 函式。

createSelector 的第一個參數會傳入一到多個 selector,第二個參數則是傳入一個函式,也就是要 memorized 的 selector,第一個傳入的多個 selector 的回傳值會作為這個 memorized selector 的參數。

例如以下的 createSelector 的第一個參數,傳入了兩個 selector,分別會取得 allPosts 和 userId,而這兩個值就會做為 memorized selector 函式的參數。

在元件內,只要傳入的參數 allPosts 和 userId 不改變,即使其他的 state 改變,selectPostsByUser 也不會重複的執行。

另外補充一點是 memorized selector 是當你在取得的 state 後又另外去做額外的事情時使用,反之如果你直接從 store 裡面透過 selector 取出值,則可以不用作 memorize 的動作。

postSlice.js

import {
  createSlice,
  createAsyncThunk,
  createSelector,
  createEntityAdapter,
} from '@reduxjs/toolkit'
import { client } from '../../api/client'

// 中間略...詳細可以看前面專案連結內的 postSlice.js

export const {
  selectAll: selectAllPosts,
  selectById: selectPostById,
  selectIds: selectPostIds,
} = postsAdapter.getSelectors((state) => state.posts)

export const selectPostsByUser = createSelector(
  [selectAllPosts, (state, userId) => userId],
  (posts, userId) => posts.filter((post) => post.user === userId)
)

UserPage.js

export const UserPage = ({ match }) => {
  const { userId } = match.params

  const user = useSelector(state => selectUserById(state, userId))

  const postsForUser = useSelector(state => selectPostsByUser(state, userId))

  // omit rendering logic
}

createSelector 運作過程:

  1. 呼叫一個 selector 時,reselect 會重新執行一次 input selectors,產生一個值
  2. 若值有任何改變,就執行 output selector,將所有結果傳給參數
  3. 若值沒有改變,不執行 output selector,回傳 cache 值

createSelector 其他使用範例:

const state = {
  a: {
    first: 5
  },
  b: 10
}

const selectA = state => state.a
const selectB = state => state.b

const selectA1 = createSelector([selectA], a => a.first)

const selectResult = createSelector([selectA1, selectB], (a1, b) => {
  console.log('Output selector running')
  return a1 + b
})

const result = selectResult(state)
// Log: "Output selector running"
console.log(result)
// 15

const secondResult = selectResult(state)
// No log output
console.log(secondResult)
// 15

createSelector 特性

  1. createSelector 只記憶最近一次的值,如果要記錄先前多筆的值,可以用 Selector Factories
const a = someSelector(state, 1) // first call, not memoized
const b = someSelector(state, 1) // same inputs, memoized
const c = someSelector(state, 2) // different inputs, not memoized
const d = someSelector(state, 1) // different inputs from last time, not memoized
  1. 傳入的參數必須符合 input selectors 所需
const selectItems = state => state.items
const selectItemId = (state, itemId) => itemId
const selectOtherField (state, someObject) => someObject.someField;

const selectItemById = createSelector(
    [selectItems, selectItemId, selectOtherField],
    (items, itemId, someField) => items[itemId]
);

const item = selectItemById(state, 42)

/*
Internally, Reselect does something like this:

const firstArg = selectItems(state, 42);  
const secondArg = selectItemId(state, 42);  
const thirdArg = selectOtherField(state, 42); // error, 42.someField 這樣會出錯
  
const result = outputSelector(firstArg, secondArg);  
return result;  
*/

2. createEntityAdapter 可以進行 normalizing Data

normalizing Data 也就是將含有 id、像列表一樣含有多個物件的陣列 state 轉換成像 hashTable 的形式,可以直接透過 id 取得資料,例如下方的 users 可以直接透過 user 的 id 去取得詳細的 user 資料,而不用將整個 users 一一遍歷取其 id 查找,而下面的格式也是經過 createEntityAdapter API 整理過後的資料格式。

{
  users: {
    ids: ["user1", "user2", "user3"],
    entities: {
      "user1": {id: "user1", firstName, lastName},
      "user2": {id: "user2", firstName, lastName},
      "user3": {id: "user3", firstName, lastName},
    }
  }
}

透過這個 API 去將相關的 state 資料做格式的統一,並且 createEntityAdapter 有提供一些 reducers 函式也能做一些常見的資料處理,像是更新某筆資料,刪除多筆資料等操作,詳細有哪些可以參考官方文件: createEntityAdapter CRUD Functions

Redux Essentials, Part 6: Performance and Normalizing Data 的範例程式碼中的 src/features/posts/postsSlice.js,可以看到 createEntityAdapter 的使用範例,以下就節錄片段程式碼做說明:

// 內建的 sortComparer 可以將 IDs 陣列做排序
const postsAdapter = createEntityAdapter({
  sortComparer: (a, b) => b.date.localeCompare(a.date),
})

// 回傳新的 entity state object,內容長這樣: {ids: [], entities: {}}.
const initialState = postsAdapter.getInitialState({
  status: 'idle',
  error: null,
})

// createEntityAdapter 產生的物件還有包括 getSelectors 函式,可以生成 selectAll、selectById 等 selector 函式
export const {
  selectAll: selectAllPosts,
  selectById: selectPostById,
  selectIds: selectPostIds,
} = postsAdapter.getSelectors((state) => state.posts)

上一篇
Day14-Redux 篇-介紹 Redux
下一篇
Day16-Redux 篇-認識 Redux Toolkit
系列文
用30天更加認識 React.js 這個好朋友33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
QQ
iT邦新手 5 級 ‧ 2023-08-04 16:34:51

想問一下之前沒接觸過 redux
redux、react-redux、redux-toolkit
這三個從哪個開始看才好?
網路上新舊資料都混在一起好混亂

harry xie iT邦研究生 1 級 ‧ 2023-08-04 16:45:23 檢舉

你發問問那麼多問題,別人認真回覆,你連謝謝都沒有,自己問 chatGPT

我要留言

立即登入留言