React Hooks 之 useFetch

前言

自 React Hooks 16.8.0 後帶來了 React hooks 這一特性。這一特性在沒有破壞性的更新下爲咱們帶來了更加舒爽的開發方式。過去咱們經常因providers,consumers,高階組件,render props 等造成「嵌套地獄」。儘管 Class Component 在某種程度上爲咱們提供了更方便的寫法以及生命週期,但同時也帶來了一些很差的地方。例如難以理解的 class 內部原理、難以測試的聲明週期。而 React Hooks 爲咱們提供了一種 Function Component 的寫法,讓咱們用更少的代碼寫出更加優雅、易懂的代碼。本文不作 React Hooks API的講述,若有不懂,請移步 Hooks 簡介javascript

發送服務端請求所面臨的問題

1. try / catch問題

在開發代碼時,咱們發送後端請求後接受到的數據,須要使用try/catch來捕獲錯誤。而每次捕獲出的錯誤可能須要打印出來以檢測bug。這樣咱們每次都會寫一樣的代碼,這樣在開發過程當中很不友好。同時有些同窗不習慣使用 try/catch 來捕獲錯誤,這就可能形成不可預計的問題。html

import React, { useCallback, useReducer, useEffect } from 'react'
import { TimeNumberType, PageType } from 'common/constant/interface'

type ParamsType = PageType & TimeNumberType

const reducer = (state: ParamsType, action: Actions) => {
  const { payload } = action
  return { ...state, ...payload }
}
const postListData = (params: ParamsType) => post('/network/api/test/getlist', params)
const initialParams = {
  pageSize: 10,
  pageNumber: 1,
  startTime: 0,
  endTime: 0
}

const ListComponent = () => {
  const [params, dispatch] = useReducer(reducer, initialState)
  const getList = async () => {
    // try catch
    try {
      const res = await postListData(params)
      console.log(res)
    } catch (err) {
      console.error(err)
    }
  }

  useEffect(() => {
    getList()
  }, [params])
}
複製代碼

demo中展現了在業務場景中發送請求的場景,當發送請求多了以後咱們會每次手動try / catch,雖然不是大問題,可是重複代碼寫多了會以爲難受...。下面看第二個功能。java

2. 請求狀態

在實際的業務場景中,咱們向後端發送請求時,每每伴隨着用戶點擊屢次,可是隻能發送一次請求的問題,這時咱們須要手動加鎖。而且在不少場景中咱們須要知道請求狀態來爲頁面設置loading。例如:react

import React, { useCallback, useReducer, useEffect } from 'react'
import { TimeNumberType, PageType } from 'common/constant/interface'
import { DateRangePicker, Table } from 'UI'

type ParamsType = PageType & TimeNumberType

const TIME = Symbol('time')
const PAGE = Symbol('page')
const reducer = (state: ParamsType, action: Actions) => {
  const { payload } = action
  return { ...state, ...payload }
}
const postListData = (params: ParamsType) => post('/network/api/test/getlist', params)
const initialParams = {
  pageSize: 10,
  pageNumber: 1,
  startTime: 0,
  endTime: 0
}

const ListComponent = () => {
  const [params, dispatch] = useReducer(reducer, initialState)
  const [loading, setLoading] = useState(false)
  const [list, setList] = useState({})
  
  const getList = async () => {
    // loading is true
    if (loading) return
    // set loading status
    setLoading(true)
    // try catch
    try {
      const res = await postListData(params)
      setList(res)
      setLoading(false)
    } catch (err) {
      console.error(err)
      setLoading(false)
    }
  }

  useEffect(() => {
    getList()
  }, [params])
  
  return (
      <div style={{ marginBottom: '20px' }}>
        <DateRangePicker
          onChange={handleDateChange}
        />
      <Table
        onPageChange={(pageNumber: number) => {
          dispatch({ payload: { pageNumber }, type: PAGE })
        }}
        list={list}
        // 數據是否正在加載,以此來判斷是否須要展現loading
        loading={loading}
      />
    </div>
  )
}
複製代碼

demo中展現了日期組件以及包含有分頁器的 Table組件,當日期發生變動,或者分頁器發生變動時,咱們須要dispatch來更新請求參數,從而發送請求。在發送請求時若是正在請求,則忽略,而不在請求時須要手動加鎖,來防止屢次請求。
同時Table須要根據請求狀態來判斷是否須要展現loading。後端

解決問題

基於以上的問題,咱們可否經過 Hooks 來封裝一個 custom hooks來解決問題。api

1. 明確目標

custom hooks 解決的問題async

  • 解決每一個函數都要統一寫try/catch的流程
  • 解決發送請求須要手動加鎖防止屢次重複請求的痛點
  • 不須要在手動useState loading,直接獲取loading值

因此咱們須要在 custom hooks 中發送請求、暴露出請求後的值、暴露 loading 狀態、以及用戶可能須要屢次請求,這就須要暴露一個勾子。在發生請求錯誤時可能須要作某些操做,因此還須要暴露在錯誤時回調的勾子函數。ide

是否當即請求並接受初始化返回值函數

業務咱們並不但願初始化的是否當即發送請求。 而且可以有初始化的返回值post

支持泛型

在TS中,開發者但願可以自定義請求的參數類型,以及請求結果的類型

2. 定義函數

useFetch 函數

import { useState, useEffect } from "react";

/**
 * 1. 解決每一個函數都要統一寫try/catch的流程
 * 2. 解決發送請求須要手動加鎖防止屢次重複請求的痛點
 * 3. 不須要在手動useState loading了~,直接獲取fetching值
 * 4. (甚至在參數發生變化時只須要傳入更改的參數就OK)已刪除
 * @param getFunction 發送請求的函數
 * @param params 參數
 * @param initRes 初始化值
 * @param execute 是否當即執行請求函數
 */

// R, P支持泛型
function UseFetch<R, P>(
  getFunction: any,
  params: P,
  initRes?: R,
  execute: boolean = true
): [
  R,
  boolean,
  (params?: Partial<P>) => void,
  (fn?: (err: any) => void) => void
] {
  type ErrorFunction = ((fn?: (err: any) => void) => void) | null;
  const [res, setRes] = useState(initRes as R);
  const [fetching, setFetch] = useState(false);
  const [failed, setFailed] = useState<ErrorFunction>(null);

  // 參數也許並非每次都完整須要 Partial<P>
  const fetchData: (params?: Partial<P>) => void = async (params?: any) => {
    if (fetching) return;
    setFetch(true);
    try {
      setRes(await getFunction(params));
      setFetch(false);
    } catch (err) {
      console.error(err);
      setFetch(false);
      failed && failed(err);
    }
  };

  const setError: ErrorFunction = fn => fn && setFailed(fn);

  // 首次執行只請求一次
  useEffect(() => {
    execute && fetchData(params);
  }, []);

  /**
   * res 返回的數據
   * fetching 是否在請求中
   * fetchData 手動再次觸發請求
   * setError 當發生請求錯誤時,須要執行的回掉函數
   */
  return [res, fetching, fetchData, setError];
}

const useFetch = UseFetch;

export default useFetch;


複製代碼

3. 如何使用

根據最初的demo咱們改造一下代碼

import React, { useCallback, useReducer, useEffect } from 'react'
import { TimeNumberType, PageType } from 'common/constant/interface'
import { DateRangePicker, Table } from 'UI'
// 導入 useFetch
import { useFetch } from 'custom-hooks' 

type ParamsType = PageType & TimeNumberType
type ListInfo = {list: Array<any>, total: number}

const TIME = Symbol('time')
const PAGE = Symbol('page')
const reducer = (state: ParamsType, action: Actions) => {
  const { payload } = action
  return { ...state, ...payload }
}
const postListData = (params: ParamsType) => post('/network/api/test/getlist', params)
const initialParams = {
  pageSize: 10,
  pageNumber: 1,
  startTime: 0,
  endTime: 0
}

const ListComponent = () => {
  const [params, dispatch] = useReducer(reducer, initialState)
  
  const [list, loading, getList] = useFetch<ListInfo, ParamsType>(
    getWithDraw,
    state,
    { list: [], total: 0 },
    false
  )

  useEffect(() => {
    getList()
  }, [params])
  
  return (
      <div style={{ marginBottom: '20px' }}>
        <DateRangePicker
          onChange={handleDateChange}
        />
      <Table
        onPageChange={(pageNumber: number) => {
          dispatch({ payload: { pageNumber }, type: PAGE })
        }}
        list={list}
        // 數據是否正在加載,以此來判斷是否須要展現loading
        loading={loading}
      />
    </div>
  )
}
複製代碼

對比代碼咱們能夠看到中間的請求的代碼被咱們幹掉了,使用 useFetch 來將狀態以及發送請求封裝在一塊兒。可以讓咱們寫更少的代碼。


同時 useFetch的第3個參數當傳入的爲 null 時,能夠模擬請求發送錯誤,這樣咱們能夠在開發時作兜底方案。

4. 也許並不想要那麼多值。

也許有些請求不須要關注請求狀態

// 解構賦值、空着就好
  const [list, , getList] = useFetch<ListInfo, ParamsType>(
    getWithDraw,
    state,
    { list: [], total: 0 },
    false
  )
複製代碼

本文完~

若有問題,歡迎指出~

相關文章
相關標籤/搜索