iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 25
0
Modern Web

寫React的那些事系列 第 25

React Day25 - Async Action 與 redux-thunk

我們已經學會Redux的概念了,不過前面的action都是同步的,每次觸發action,state都會馬上被更新,今天要來介紹的是非同步action,原本我們的action creator都是回傳一個action object,透過redux-thunk,action creator可以傳回一個function,而這個function就是thunk。

關於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


上面最後一個範例是非同步執行的概念,而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);
};

使用redux-thunk

首先也是透過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:

  1. ADD_TASK_REQUEST: 發出儲存task請求。
  2. ADD_TASK_SUCCESS: 收到儲存task成功callback。
  3. ADD_TASK_FAILURE: 收到儲存task失敗callback。

我們使用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可以這樣設定:

  • 當發出request請求時,loading設定為true。
  • 當收到success或是failure時,loading設定為false。
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後和後端要如何串接囉!


上一篇
React Day24 - Middleware運用 與 Logger for Redux
下一篇
React Day26 - Immutablejs
系列文
寫React的那些事31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言