iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 29
1
Modern Web

從比入門再往前一點開始,一直到深入React.js系列 第 29

【Day.29】React進階 - 以Redux Thunk處理非同步資料流

很多時候,我們的state必須要透過HTTP Request從後端取得。然而發送Request常用的fetch或是axios是非同步的。雖然我們可以透過以下方式把資料送進去Redux:

fetch( "URL", {
        method: "GET"
    })
    .then(res => res.json())
    .then(data => {
        dispatch({type:"TYPE", payload: {data}});
    })
    .catch(e => {
        /*發生錯誤時要做的事情*/
    }
)

但最理想的狀況還是讓這個fetch的過程被模組、抽象化,也就是不應該還要讓UI繪製程式還要自己去call fectch API。我們希望UI繪製程式只需要呼叫一個函式,從fetch到更新Redux的這串過程都會完成

不論是在Flux,還是傳統的MVC、MVP、MVVM觀念下,都希望把資料處理的程式抽離UI繪製的程式,而不是讓兩者混雜在一起

講白一點,我們的流程本來是:

  1. 操作者呼叫dispatch
  2. Redux判斷action
  3. Redux根據action對state做出對應修改

現在我們希望流程改成這樣:

  1. 操作者呼叫dispatch
  2. 一個遇到非同步事件,就會等到非同步事件結束才再次呼叫dispatch、傳遞action的模組程式
  3. Redux判斷action
  4. Redux根據action對state做出對應修改

一般會把2這種在本來行為之間(1和3)的加工過程稱為middleware(中介層)。

Redux-Thunk

Redux-Thunk就是一個簡化Redux處理非同步事件的中介層套件。它的運作流程是這樣的,基本上就跟我們剛剛說的差不多:


上圖來源

Redux middleware與Redux-Thunk的使用

接下來我們會實際操作一次Redux-Thunk,試著把MenuItem的資料改成從後端取得。資料會用我放在自己github的台灣的縣市列表JSON檔

{
    "cityList":[
        "臺北市",
        "基隆市",
        "新北市",
        (略......)
    ]
}

1. 安裝

請打開terminal,輸入:

npm install redux-thunk --save

2. 建立src/model/action.js

一般會在這裡以變數統一管理action字串。不過這裡我們先拿來放等等要定義的fetch

在src/model/action.js中,定義一個函式,把item改成fetch函式得到的資料。Redux-Thunk會把dispatch函式當成函式的參數傳入。我們則要在非同步事件結束後再次呼叫dispatch,給予對應的action和payload

因為現在我們的reducer還沒有這種一次修改所有資料的action,我們先加一個SET_ITEM,等等再加回reducer中。

  • src/model/action.js
export const fetchCityItem = () => {
    return (dispatch) => {
        fetch( "https://raw.githubusercontent.com/JiaAnTW/mask/master/dist.json", {
            method: "GET"
        })
        .then(res => res.json())
        .then(data => {
            dispatch({
                type: "SET_ITEM",
                payload: {itemNewArr: data["cityList"]}
            });
        })
        .catch(e => {
            console.log(e);
        })
    };
};

3. 加入Redux-Thunk到Redux中

Redux提供了applyMiddleware這個函式來讓我們安裝middleware到Redux中。用法是將applyMiddleware(中介層1,中介層2,...)放在createStore的第二個參數中。

現在,請引入Redux-Thunk的thunk和Redux的applyMiddleware,並加入我們的store中:

  • src/model/store.js
import {createStore, applyMiddleware } from "redux";
import {itemReducer} from "./reducer.js";
import thunk from "redux-thunk";

const itemStore = createStore(itemReducer,applyMiddleware(thunk)); 

export {itemStore};

這樣使用Redux-thunk的架構就完成了。

4. 補回reducer處理 SET_ITEM 的case

  • src/model/reducer.js
const initState = {
    menuItemData: [
        "Like的發問",
        "Like的回答",
        "Like的文章",
        "Like的留言"
    ],
  };
  
const itemReducer = (state = initState, action) => {
    switch (action.type) {
      case 'ADD_ITEM': {
        const menuItemCopy = state.menuItemData.slice();
        return { menuItemData: [action.payload.itemNew].concat(menuItemCopy) };
      }
      case 'SET_ITEM': {
        return { menuItemData: action.payload.itemNewArr };
      }
      case 'CLEAN_ITEM': {
        return { menuItemData: [] };
      }
      default:
        return state;
    }
};

export {itemReducer};

5. 在需要的地方,以dispatch呼叫fetchCityItem

觸發Redux-Thunk的方式,是在需要的地方呼叫

dispatch( 剛剛定義的非同步函式() );

也就是你可以在src/page/MenuPage新增一個按鈕:

<button onClick={()=>{
    dispatch(
        fetchCityItem()
    ); 
}}>抓取並修改menuItem</button>

按下去之後,Redux就會根據我們剛剛定義的內容,先執行發送Http Request,等資料回來,才執行dispatch,把action和剛剛放入payload的縣市資料丟到reducer去更新。

  • src/page/MenuPage.js
import React, { useState, useReducer,useMemo,useEffect} from 'react';
import useMouseY from '../util/useMouseY';

import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';

import { useSelector, useDispatch } from 'react-redux';
import { fetchCityItem } from '../model/action';

const reducer = function(state, action){
    switch(action.type){
        case "SWITCH":
            return !state;
        default:
            throw new Error("Unknown action");
    }
}

const MenuPage = () =>{
    const [isOpen, isOpenDispatch] = useReducer(reducer,true);

    const menuItemData = useSelector(state => state.menuItemData);
    const dispatch = useDispatch();

    let menuItemArr = useMemo(()=>menuItemData.map((wording) => <MenuItem text={wording} key={wording}/>),[menuItemData]);

    return (
        <OpenContext.Provider value={{ 
            openContext: isOpen, 
            setOpenContext: isOpenDispatch
        }} >
            <Menu title={"Andy Chang的like"}>
                {menuItemArr}
            </Menu>
            <button onClick={()=>{
                dispatch({
                    type: "ADD_ITEM",
                    payload: {itemNew:"測試資料"}
                }); 
            }}>更改第一個menuItem</button>
            <button onClick={()=>{
                dispatch(
                    fetchCityItem()
                ); 
            }}>抓取並修改menuItem</button>
        </OpenContext.Provider>
    );
}

export default MenuPage;

參考資料

Thunks in Redux: The Basics


上一篇
【Day.28】React進階 - 導入Redux,讓元件溝通更簡潔
下一篇
【Day.30】React進階 - Styled-Components: React的CSS解決方案 | 系列總結
系列文
從比入門再往前一點開始,一直到深入React.js30

尚未有邦友留言

立即登入留言