起因

小组决定通过 Taro 来开发,但是自己对 React 的印象已经很模糊了。于是需要重新学习 React 🙃。而在学习 Hook 概念并了解相关 demo、example 时看到了这样一篇 文章。让我们先来看看他的最终代码。

import React, {
  useState,
  useEffect,
  useReducer,
} from 'react';
import axios from 'axios';

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return { ...state, isLoading: true, isError: false };
    case 'FETCH_SUCCESS':
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: action.payload,
      };
    case 'FETCH_FAILURE':
      return {
        ...state,
        isLoading: false,
        isError: true,
      };
    default:
      throw new Error();
  }
};

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  useEffect(() => {
    let didCancel = false;

    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });

      try {
        const result = await axios(url);

        if (!didCancel) {
          dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
        }
      } catch (error) {
        if (!didCancel) {
          dispatch({ type: 'FETCH_FAILURE' });
        }
      }
    };

    fetchData();

    return () => {
      didCancel = true;
    };
  }, [url]);

  return [state, setUrl];
};

第一感觉很棒啊,又把状态码和 loading 给做了。然鹅,实际迁移到项目时,却发现不太实用。。所以需要做一些修改。

优点 不足
1. 封装了状态码和 loading,无需在具体组件中考虑。 1. 只支持 GET 请求。
2.通过 didCancel 来避免组件 unmount 后仍 set 请求结果 2. 请求立马执行,且因为只能在函数最外层调用 Hook,不能在循环、条件判断或者子函数中调用。所以无法针对性加载数据。

Enhanced useDataApi Hook

首先我们先直接展示一下修改完的代码。

import Taro, {useEffect, useReducer, useState,} from '@tarojs/taro'

import {HEADER_MADPILL_TOKEN_KEY, MADPILL_RESPONSE_CODE} from '../constants'
import {getToken} from '../utils/login'

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'REQUEST_INIT':
      return {...state, isLoading: true, statusCode: undefined};
    case 'REQUEST_SUCCESS':
      console.log('REQUEST_SUCCESS')
      console.log(action.payload)
      return {
        ...state,
        isLoading: false,
        statusCode: MADPILL_RESPONSE_CODE.OK,
        data: action.payload,
      };
    case 'REQUEST_FAILURE':
      console.log('REQUEST_FAILURE')
      console.log(action.errorCode)
      return {
        ...state,
        isLoading: false,
        statusCode: action.errorCode,
      };
    default:
      throw new Error();
  }
};

/**
 *
 * @param option {object}
 * @param option.requestMethod {Taro.request.method}
 * @param option.requestUrl {string}
 * @param [option.initialResultData] {object}
 * @param [option.requestData] {object | array}
 * @param [option.execNow=false] {boolean} set true to send the REQUEST on every change
 * @return {[S, (value: (((prevState: {method: *, data: *, url: string}) => {method: *, data: *, url: string}) | {method: *, data: *, url: string})) => void]}
 */
const useDataApi = ({
                      requestMethod,
                      requestUrl,
                      requestData,
                      initialResultData,
                      execNow = false,
                    }) => {

  const [request, setRequest] = useState({
    method: requestMethod,
    url: requestUrl,
    data: requestData,
    exec: execNow,
  });

  const [resultState, resultDispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    statusCode: undefined,
    data: initialResultData,
  });

  useEffect(() => {
    let didCancel = false;

    if (request.exec) {
      console.log('now exec')
      resultDispatch({
        type: 'REQUEST_INIT'
      });
      console.log(request.data)

      getToken({
        success: token => {
          let requestHeader = {}
          requestHeader[HEADER_MADPILL_TOKEN_KEY] = token
          console.log(requestHeader)

          Taro.request({
            url: `${HOST}/${request.url}`,
            method: request.method,
            header: requestHeader,
            data: request.data,
            success: result => {
              console.log('success')
              console.log(result)
              const madpillResult = result.data
              if (!didCancel && result.statusCode === 200 && madpillResult.code === MADPILL_RESPONSE_CODE.OK) {
                resultDispatch({
                  type: 'REQUEST_SUCCESS',
                  payload: madpillResult.data
                });
              } else {
                resultDispatch({
                  type: 'REQUEST_FAILURE',
                  errorCode: madpillResult.code ? madpillResult.code : madpillResult.status
                });
              }
            },
            fail: error => {
              console.log('fail')
              console.log(error)
              if (!didCancel) {
                resultDispatch({type: 'REQUEST_FAILURE', errorCode: 400});
              }
            }
          })
        }
      })
    }

    return () => {
      didCancel = true;
    };
  }, [request]);

  return [resultState, setRequest];
};

export default useDataApi

对之前的两个问题,其实都是通过在初始化时新增初始参数(requestMethodnowExec)来解决的。

使用 Demo

简单 Demo

在此 demo 中只需加载一次数据,所以只需要解构出相关信息。

  const [{data, isLoading, statusCode}] = useDataApi({
    requestMethod: ...,
    requestUrl: ...,
    initialResultData: [],
    execNow: true,
  })

  // 数据加载结束
  useEffect(() => {
    ...
  }, [data])

  // 加载中的相关处理
  useEffect(() => {
    if (isLoading) {
      ...
    } else {
      ...
    }
  }, [isLoading])

  // 错误的处理
  useEffect(() => {
    if (statusCode !== MADPILL_RESPONSE_CODE.OK) {
      ...
    }
  }, [statusCode])

复杂 Demo

在此我们考虑一个药品信息的加载场景。他可能是新建药品,也可能是查看药品信息。首先定义一个药品类型,以便之后传入 initialResultData

  const [medicine, setMedicine] = useState({
    id: undefined,
    name: '',
    producedDate: '',
    expireDate: '',
    group: {
      id: '',
      name: '',
    },
    tags: [],
    description: '',
    reminders: JSON.stringify([]),
    indication: JSON.stringify({
      content: ''
    }),
    contraindication: JSON.stringify({
      content: ''
    }),
  })

第二,需要定义新增药品时的请求。

	const [{data: warehouseRequestedData, isLoading: warehouseLoading, statusCode: warehouseStatusCode}, warehouseRequest] = useDataApi({
    requestMethod: 'GET',
    requestUrl: `warehouse/${props.routerParams.warehouseId}`,
    initialResultData: {},
  })

  // warehouseRequest 加载结果返回后设置本组件中药品 medicine 的相关信息
  useEffect(() => {
    // console.log('warehouseRequest finish')
    // console.log(warehouseRequestedData)
    setMedicine(preMedicine => {
      return {
        ...preMedicine,
        ...warehouseRequestedData,
      }
    })
  }, [warehouseRequestedData])

然后定义查看药品时的请求。

  const [{data: medicineRequestedData, isLoading: medicineLoading, statusCode: medicineStatusCode}, medicineRequest] = useDataApi({
    requestMethod: 'GET',
    requestUrl: `drugs/${props.routerParams.medicineId}`,
    initialResultData: medicine,
  })

  // medicineRequest 加载结果返回后设置本组件中药品 medicine 的相关信息
  useEffect(() => {
    // console.log('medicineRequest finish')
    // console.log(medicineRequestedData)
    setMedicine(medicineRequestedData)
  }, [medicineRequestedData])

第三,我们需要实现定义在页面初始化的 useEffect() 中的 initData() 方法,他需要根据传入的不同参数区分加载的内容。

  const initData = () => {
    if (props.routerParams.action === MADPILL_ADD_CONFIG.ACTION_ADD) {
      // 新增界面
      setDefaultDateWhenAdd()
      if (props.routerParams.addMode === MADPILL_ADD_CONFIG.ADD_MODE_MADPILL) {
        // 从仓库新增
        warehouseRequest(preRequest => {
          return {
            ...preRequest,
            exec: true
          }
        })
      } else if (props.routerParams.addMode === MADPILL_ADD_CONFIG.ADD_MODE_DIRECT) {
        // 直接新增
        setMedicine(preMedicine => {
          return {
            ...preMedicine,
            name: props.routerParams.manualName,
          }
        })
      }
    } else if (props.routerParams.action === MADPILL_ADD_CONFIG.ACTION_REVIEW) {
      // 查看修改删除界面
      medicineRequest(preRequest => {
        return {
          ...preRequest,
          exec: true
        }
      })
    }
  }

最后,我们来处理加载中 loading 和错误处理。

  // 加载中的相关处理
  useEffect(() => {
    // console.log(`warehouseLoading: ${warehouseLoading}`)
    // console.log(`medicineLoading: ${medicineLoading}`)
    if (warehouseLoading || medicineLoading) {
      Taro.showLoading({
        title: '加载中(/ω\)',
        mask: true,
      })
    } else {
      Taro.hideLoading()
    }
  }, [warehouseLoading, medicineLoading])

  // 错误的处理
  useEffect(() => {
    // console.log(`warehouseStatusCode: ${warehouseStatusCode}`)
    // console.log(`medicineStatusCode: ${medicineStatusCode}`)
    if (!(warehouseStatusCode === undefined || warehouseStatusCode === MADPILL_RESPONSE_CODE.OK)) {
      Taro.showToast({
        title: '初始化药品失败(ToT)/~~~',
        icon: 'none'
      })
    }

    if (!(medicineStatusCode === undefined || medicineStatusCode === MADPILL_RESPONSE_CODE.OK)) {
      Taro.showToast({
        title: '查找药品失败(ToT)/~~~',
        icon: 'none'
      })
    }
  }, [warehouseStatusCode, medicineStatusCode])

参考