【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


<<:  见习村29 - Sum of Intervals

>>:  Day29 利用web发送讯息(上)

Day 16 | 同步与非同步- Coroutines的Scope

Scope Scope 指得是Coroutines 可以作用的范围。 在Main thread上或I...

[Day30] 今天是最後一天啦~

今天是我最後一次以git的主题跟各位沟通啦~ 说实话,觉得要坚持30天,天天发文真的有点难馁XD ...

Day02 X 为什麽要在前端做效能优化?

大家好!虽然今天是 Day 2,不过严格来说是系列文的第一天。今天要来谈谈「为什麽我们需要在前端做...

Hello WebGL

大家好,大家都叫我西瓜。因为想转职写游戏,而游戏中会让人第一个想到、也是能在第一瞬间吸引人的就是画面...

D13: 工程师太师了: 第7话

工程师太师了: 第7话 杂记: 这次的COVID-19 疫情, 究竟对我们产生了什麽影响? 很多人会...