redux-thunk 是簡化 Redux 處理「非同步事件」的 Middleware 套件。Redux Middleware 是用來在 Action 進入到 Reducer 之前,可以讓我們做一些介入 Action 的時間點。
redux-thunk 會在 Middleware 執行非同步事件,然後在非同步事件完成後,再決定實質上要對 Store 發出什麼 Action 和 Payload。
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秒後執行
如果是使用 Create-React-App 開發的專案,使用 npm 安裝如下
npm install redux-thunk --save
為了示範方便,這裡我們沿用 前一篇文章的 Sandbox 模版,使用 Fork 製作加上 redux-thunk 的功能 (記得要加上 react-redux 的 Dependencies)
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 來幫助我們處理非同步。
在此將之前提到 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(剛剛定義的非同步ActionCreator)
import { setFilterAsync } from "../store/actions";
...
dispatch(setFilterAsync(filterTitle))}
...
這時候切換 Filter 會發現資料會延遲三秒才做出變化。
完整程式碼操作:https://codesandbox.io/s/react-todomvc-redux-middleware-thunk-delay-8ji3kx
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 剛剛示範做 delay 的範例專案模版,微調程式如下:
原本是
import { setFilterAsync } from "../store/actions";
...
dispatch(setFilterAsync(filterTitle))}
...
改成
import { setFilter } from "../store/actions";
...
dispatch(setFilter(filterTitle))
...
<span style={{ zIndex: 10 }}>
Load Online Todos
</span>
畫面會變成這樣
這裡的 CSS 有做一些調整,大家可以在後面提供的執行結果,再來看要怎麼改,這邊先理解觀念後,再動手去做。
// store/actions/actionType.js
export const FETCH_TODOS = "FETCH_TODOS";
// 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);
});
};
};
// 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];
...
}
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
透過 redux-thunk 幫助我們可以在 Redux 中執行非同步的處理,不過當專案要面對的更複雜的情境,有時候我們需要處理取消非同步請求,或是處理更複雜的資料流時,redux-thunk 就不敷使用。
接下來要介紹的 redux-observable,就可以讓專案處理更多非同步的複雜情境。
https://ithelp.ithome.com.tw/articles/10187438
https://pjchender.dev/react/redux-thunk/
https://note.pcwu.net/2017/03/20/redux-thunk-intro/