我們已經學會Redux的概念了,不過前面的action都是同步的,每次觸發action,state都會馬上被更新,今天要來介紹的是非同步action,原本我們的action creator都是回傳一個action object,透過redux-thunk,action creator可以傳回一個function,而這個function就是thunk。
在Wiki解釋的thunk:
In computer programming, a thunk is a subroutine that is created, often automatically, to assist a call to another subroutine.
Thunk就是一個會回傳另一個函式的函式,可以先看這篇Thunk 函數的含義和用法,在文章中的範例會覺得有點像Currying,一樣可以把多個參數的函式簡化成單一參數的函式,而且內部會回傳另一個函式,但他們有意義上的不同,可以再參考Currying vs partial application的說明。
以下用我們之前Currying的例子,改成ES6寫法來說明:
// 原本的函式,傳入多個參數
const add = (x, y) => {
console.log(x + y);
};
add(3, 4); // 立即執行
// Curring後函式,傳入單一參數
const addWithCurrying = (x) => {
return (y) => {
console.log(x + y);
};
};
addWithCurrying(3)(4); // 立即執行
// 使用thunk包住一個delay的機制
const addWithThunk = (func, x) => {
return (y) => {
setTimeout(() => {
func(x, y);
}, 3000)
};
};
addWithThunk(add, 3)(4); // 3秒後執行
上面最後一個範例是非同步執行的概念,而redux-thunk就是為了讓我們能使用非同步執行action的middleware,如redux-thunk官網說:
A thunk is a function that wraps an expression to delay its evaluation.
實作thunk middleware的概念如下:
const thunkMiddleware = ({ dispatch, getState }) => next => action => {
// 判斷傳入的action是否為thunk函式
if (typeof action === 'function') {
// 是的話,把dispatch和getState傳給這個thunk函式
return action(dispatch, getState);
}
// 不是的話,把action傳給下一個middleware
return next(action);
};
首先也是透過npm來安裝:
npm install redux-thunk --save
建立store時,把redux-thunk加入:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
在action中,當我們想使用async action,我們就可以這樣寫:
export function addTask(task){
return {
type: types.ADD_TASK,
task
};
}
export function addTaskAsync(task){
return (dispatch) => {
setTimeout(() => {
// 一秒後dispatch addTask()
dispatch(addTask(task));
}, 1000);
};
}
這時候,我們可以把原先從TodoAppContainer傳入的的addTask改成呼叫addTaskAsync看看:
// <TodoAdd addTask={todosActions.addTask} />
<TodoAdd addTask={todosActions.addTaskAsync} />
當然,在我們範例中過一秒後才完成add task有點奇怪,但現實情況是會post資料給後端儲存到資料庫中,而通常有和後端串接部分,不會只有一個action而已,因為發出請求和接到callback通常會有一段時間差,從UI上來看我們還需要在畫面上render loading,讓user知道正在儲存資料,所以我們至少會有三種action:
我們使用fetch來實作,在action中我們可以這樣寫:
export function addTaskRequest(){
return {
type: 'ADD_TASK_REQUEST'
};
}
export function addTaskSuccess(task){
return {
type: 'ADD_TASK_SUCCESS',
task
};
}
export function addTaskFailure(err){
return {
type: 'ADD_TASK_FAILURE',
err
};
}
export function addTask(task){
return (dispatch) => {
dispatch(addTaskRequest());
return fetch('http://www.domain.com/saveData', {
method: 'POST',
body: JSON.stringify({
task
})
})
.then(response => {
dispatch(addTaskSuccess(task));
})
.catch(err => dispatch(addTaskFailure(err)));
};
}
假設我們有一個控制是否顯示loading畫面的state,則在reducer可以這樣設定:
export default function loading(state = false, action) {
switch (action.type) {
case 'ADD_TASK_REQUEST':
return true;
case 'ADD_TASK_SUCCESS':
case 'ADD_TASK_FAILURE':
return false;
default:
return state;
}
}
我把目前的範例,先加了一個陽春loading畫面XD,先模擬add task時會發生的async狀況,目前的檔案已經放在Git上,可以執行起來看看新增task的感覺,應該就可以想像加上redux-thunk後和後端要如何串接囉!