iT邦幫忙

2022 iThome 鐵人賽

DAY 23
0
Modern Web

開始搞懂React生態系系列 第 23

Day 23 Redux 非同步 Action 解決方案 - redux-thunk

  • 分享至 

  • xImage
  •  

說明

redux-thunk 是簡化 Redux 處理「非同步事件」的 Middleware 套件。Redux Middleware 是用來在 Action 進入到 Reducer 之前,可以讓我們做一些介入 Action 的時間點。

redux-thunk 會在 Middleware 執行非同步事件,然後在非同步事件完成後,再決定實質上要對 Store 發出什麼 Action 和 Payload。

Thunk Function

Thunk 本身的含義是指一個會回傳另一個函式的函式。

通常 Thunk 會用來包住 delay 的機制,所以傳入的 Function 不會立即執行,而是等 delay 結束後,才回呼該傳入的 Function 執行完後續的功能。

// 原本的函式,傳入多個參數
const add = (x, y) => {
  console.log(x + y);
};
add(5, 10); // 立即執行
// 使用thunk包住一個delay的機制
const addWithThunk = (func, x) => {
  return (y) => {
    setTimeout(() => {
      func(x, y);
    }, 3000)
  };
};
addWithThunk(add, 5)(10); // 3秒後執行

使用 redux-thunk

安裝 redux-thunk

如果是使用 Create-React-App 開發的專案,使用 npm 安裝如下

npm install redux-thunk --save

為了示範方便,這裡我們沿用 前一篇文章的 Sandbox 模版,使用 Fork 製作加上 redux-thunk 的功能 (記得要加上 react-redux 的 Dependencies)

把 redux-thunk 加入 store

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducers from './reducers';

const store = createStore(
  reducers,
  applyMiddleware(thunk)
);

如果套用前篇文章的 Sandbox 寫法,程式碼如下

// middlewares/index.js
import { applyMiddleware } from "redux";
import logger from "./logger";
import crashReporter from "./crashReporter";
import thunk from "redux-thunk";

const enhancers = applyMiddleware(
    logger, 
    crashReporter,
    thunk
);

export default enhancers;

現在 Store 除了有logger/crashReporter middleware 的功能,也加上了 redux-thunk middleware 來幫助我們處理非同步。

製作非同步的 Action Creator

在此將之前提到 Thunk Function 概念,應用在 redux-thunk 上,便是包裝一個需要非同步處理的 Action Creator 為一個 thunk 。

原本 Action Creator 是回傳一個 Object 型態的 Action;而若是「thunk化」的 Action Creator,則是回傳一個 Function。

const setFilter = (filter) => {
  return {
    type: SET_FILTER,
    filter
  };
}

// 非同步的 Action Creator
const setFilterAsync = (filter) => {
  return (dispatch) => {
    setTimeout(() => {
      // 三秒後dispatch setFilter()
      dispatch(setFilter(filter));
    }, 3000);
  };
}

透過加上 redux-thunk middleware,其實不需要管 Action Creator 回傳的究竟是 Object 型態的 Action 還是 Thunk Function,因為它的原始碼底層就會判斷。如果是 function,就會把 dispatch、getState 等放進 Thunk Function 型態的 action 執行,回傳得到 Object 型態的 Action 後,再交給 reducers。

// redux-thunk 原始碼
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    // action 就是透過 action creators 傳進來的東西,
    // 在 redux-thunk 中會是 async function
    if (typeof action === 'function') {
      // 在這裡回傳「執行後的 async function」
      return action(dispatch, getState, extraArgument);
    }
    // 如果傳進來的 action 不是 function,則當成一般的 action 處理
    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

元件 dispatch 非同步的 Action Creator

dispatch(剛剛定義的非同步ActionCreator)
import { setFilterAsync } from "../store/actions";
...
dispatch(setFilterAsync(filterTitle))}
...

這時候切換 Filter 會發現資料會延遲三秒才做出變化。

完整程式碼操作:https://codesandbox.io/s/react-todomvc-redux-middleware-thunk-delay-8ji3kx

使用 redux-thunk 串接 API

redux-thunk 是單純用來讓開發者能夠在 redux 中呼叫非同步請求,所以我們也可以使用它來處理 API 請求。

很多時候,state 必須要過 Http Request 向後端發 API 請求後取得,然後發送 Request 常用的 fetch 或是 axios 都是非同步的機制。

redux-thunk 會在 Middleware 執行非同步事件,然後在非同步事件完成後,再決定實質上要對 Store 發出什麼 Action 和 Payload。

在這邊我們可以試試看使用取得 TODOS 列表的 FAKE API,來為之前做的 Todos App 加上 FETCH_TODOS 的功能。

Fork 及 微調範例專案模版

Fork 剛剛示範做 delay 的範例專案模版,微調程式如下:

  • 把 Filter 改成不要延遲發出。

原本是

import { setFilterAsync } from "../store/actions";
...
dispatch(setFilterAsync(filterTitle))}
...

改成

import { setFilter } from "../store/actions";
...
dispatch(setFilter(filterTitle))
...
  • 加上 Load Online Todos 的按鈕
<span style={{ zIndex: 10 }}>
Load Online Todos
</span>

畫面會變成這樣

這裡的 CSS 有做一些調整,大家可以在後面提供的執行結果,再來看要怎麼改,這邊先理解觀念後,再動手去做。

製作非同步的 Action Creator

  • 加上新的 Action Type
// store/actions/actionType.js
export const FETCH_TODOS = "FETCH_TODOS";
  • 加上新的 Action Creator
// store/actions/index.js
import {FETCH_TODOS, ...} from "./actionTypes";

export const fetchTodos = (data) => {
  return {
    type: FETCH_TODOS,
    data
  };
};

export const fetchTodosAsync = () => {
  // 取得 user 1 的 todos 列表
  const url = "https://jsonplaceholder.typicode.com/users/1/todos";
  return (dispatch) => {
    fetch(url, {
      method: "GET"
    })
      .then((res) => res.json())
      .then((data) => {
        // 此 Todos 清單會回傳20筆,這裡讓它縮小成5筆
        const slicedData = data.slice(0, 4);
        // 等 API 請求回應後,才繼續執行 fetchTodos Action
        dispatch(fetchTodos(slicedData));
      })
      .catch((e) => {
        console.log(e);
      });
  };
};

在 Reducer Fuction 加上 FETCH_TODOS 的對應

// store/reducers/todosReducer.js
import {FETCH_TODOS, ...} from "./actionTypes";

switch (action.type) {
  ...
  case FETCH_TODOS:
    const newTodos = action.data.map(
      ({ id, title, completed }) => {
      return {
        id,
        text: title,
        completed
      };
    });
    return [...newTodos];
  ...
}

元件 dispatch 非同步的 Action Creator

  • 在新增的 Load Online Todos 加上 onClick 事件,dispatch(剛剛定義的非同步ActionCreator)
import { setFilter, fetchTodosAsync } from "../store/actions";
...
<span
  style={{ zIndex: 10 }}
  onClick={() => {
    dispatch(fetchTodosAsync());
  }}
>
  Load Online Todos
</span>

完成畫面及完整程式碼

完成後,按下「Load Online Todos」就會去打 API 要求 User 1 的 Todos 清單。

完整程式碼操作:https://codesandbox.io/s/react-todomvc-redux-middleware-thunk-api-r5bwhh

Next

透過 redux-thunk 幫助我們可以在 Redux 中執行非同步的處理,不過當專案要面對的更複雜的情境,有時候我們需要處理取消非同步請求,或是處理更複雜的資料流時,redux-thunk 就不敷使用。

接下來要介紹的 redux-observable,就可以讓專案處理更多非同步的複雜情境。

Reference

https://ithelp.ithome.com.tw/articles/10187438

https://pjchender.dev/react/redux-thunk/

https://medium.com/frochu/%E9%80%81%E8%AE%93%E4%BD%A0%E7%9A%84action%E8%83%BD%E4%BD%9C%E6%9B%B4%E5%A4%9A-redux-thunk-c07bc5488e48

https://note.pcwu.net/2017/03/20/redux-thunk-intro/


上一篇
Day 22 Redux Middleware 基本運作原理
下一篇
Day 24 Redux 非同步 Action 解決方案 - redux-observable
系列文
開始搞懂React生態系30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言