This is the first day of my participation in the August Challenge. For details, see:August is more challenging

preface

I want to implement a complete set of useFetchData Hooks that can be used in large projects. There are many implementations on the Internet, but I haven’t found one that fits me. Here is a record of the process, hope to help you a little bit.

[The functions are as follows] :

  • Custom hooks, consistent use, internal catch errors, return data andloading
  • Fetch is encapsulated in Typescript, and functions such as cancel request and front-end timeout are implemented

Demo: awesome-use-fetch-data_Demo

Demo warehouse address -> awesome-use-fetch-data_Repo

This article is divided into the following steps to explain the implementation process: 1 - Why do you want to encapsulate a useFetchData Hooks library? 2 - Encapsulate Typescript with a front-end timeout cancel, page destroy/route jump cancel, and fetch with loading. Implement a full useFetchData versionCopy the code

Why should I encapsulate a useFetchData Hooks

First, why implement such a Custom Hooks? There are two reasons:

  • One, if you use a lot of it in your projectHooksWhen you develop code, you can’t avoid encapsulating highly reusableCustom Hooks

If you’re still copying and pasting code, you’ll be able to read this article with some benefit.

  • Second, in a business scenario, most components of every page will request data interface rendering, so high reuse encapsulation of the request can improve development efficiency

Specifically, I wrote two pseudo-codes to improve development efficiency for reference as follows:

  • Before packaging
// page1.jsx
import { useState, useEffect } from 'react';
import axios from 'axios';

export default function Page1() {
   const [data, setData] = useState([]);
   const [loading, setLoading] = useState(false);
   
   // The component is loaded and requests data
   useEffect(() = > {
      setLoading(true);
      axios.get('/user/list', { params: { ID: 12345}})// Update the data
      .then(function(res) {
          setData(res.data);
          setLoading(false);
      })
      .catch(function(error) {
          console.log(error);
          setLoading(false);
      });
   }, [])
   
   return (
       <Table loading={loading} data={data} columns={columns} />)}// page2.jsx
import { useState, useEffect } from 'react';
import axios from 'axios';

export default function Page2() {
   const [list, setList] = useState([]);
   const [loading, setLoading] = useState(false);
   
   // The component is loaded and requests data
   useEffect(() = > {
      setLoading(true);
      axios.post('/article/list', { page: 'Fred'.pageSize: 'Flintstone'})
      .then(function (res) {
          setList(res.data);
          setLoading(false);
      })
      .catch(function (error) {
          console.log(error);
          setLoading(false);
      });
   }, [])
   
   return (
       <Table loading={loading} data={list} columns={columns} />)}Copy the code
  • After the encapsulation
// page1.jsx

// page1.jsx
import { useState, useEffect } from 'react';
import useFetchData from 'use-fetch-data';

export default function Page1() {
   const [data, setData] = useState([]);
   
  const options =  { params: { ID: 12345}};const { data, loading } = useFetchData('/user/list', options);
   
   return (
       <Table loading={loading} data={data} columns={columns} />)}// page2.jsx
import { useState, useEffect } from 'react';
import useFetchData from 'use-fetch-data';

export default function Page2() {
   
  const options =  { method: 'POST'.data: { page: 'Fred'.pageSize: 'Flintstone'}};
  const { data, loading } = useFetchData('/article/list', options);
   
   return (
       <Table data={data} columns={columns} />)}Copy the code

From the above two pieces of code, we can clearly see the code comparison before and after encapsulation. Before encapsulation, each business component handles the request internally, but also handles the data and loading state. Then, it is ok for one component to have two components. Then the project has a lot of redundant code for CV operations.

Therefore, it can be seen that useFetchData Hooks can greatly simplify our business code. It is really a very good choice to abstract a common component or method into Custom Hooks. We hope that you can use them in your daily life. Build your own react-use Hooks Utils.

UseFetchData Hooks (” fetch “); useFetchData Hooks (” fetch “); useFetchData Hooks (” fetch “); You just have to choose the request library that you’re good at.

Encapsulates a multifunction fetch based on Typescript

As mentioned above, the core of this article is not to encapsulate FETCH, because FETCH is not a library that many people like to use, maybe many of you like to use Axios, it doesn’t matter, anyway, you can do the same thing, you can request your own library replacement, I just like fetch. Ts-fetch implements the following functions:

  • 1 – Internal handling exceptions (need to be returned by convention with the backend)

  • 2 – The front-end automatically times out. When the request and response time exceeds a certain threshold, the front-end considers it timeout (which can be overridden by passing parameters).

  • 3 – Cancel requests with AbortController (page destruction/route jump)

T-fetch code address -> Useful -kit, personal TypeScript used in general, if there are big people have a better encapsulation method, you can leave a message or warehouse directly to build, very grateful.

The following directly paste the package request library code, write articles, is just a simple package, everyone in the business can also according to the business characteristics, again to adapt to the degree of their project, such as path parameters API this can also support:

// This is the isomorphic fetch that can be performed on both the server and client side
import fetch from 'isomorphic-unfetch';
// Query formatted plugins can be implemented by themselves
import qs from 'query-string';
// Catch a hint of the internal handling of exceptions, consistent with the UI library your project is using
import { message } from 'antd';

function filterObject(o: Record<string, string>, filter: Function) {
  const res: Record<string, string> = {};
  Object.keys(o).forEach(k= > {
    if(filter(o[k], k)) { res[k] = o[k]; }});return res;
};

export enum EHttpMethods {
  GET = 'GET',
  POST = 'POST',
  PUT = 'PUT',
  PATCH = 'PATCH',
  DELETE = 'DELETE'
}

type ICustomRequestError = {
  status: number;
  statusText: string;
  url: string;
}

function dealErrToast(err: Error& ICustomRequestError, abortController? : AbortController) {
  switch(err.status) {
    case 408: {
      abortController && abortController.abort();
      (typeof window! = ='undefined') && message.error(err.statusText);
      break;
    }
    default: {
      console.log(err);
      break; }}}/ * * *@description: Specifies the type of the request header */interface IHeaderConfig { Accept? : string;'Content-Type': string;
  [propName: string]: any;
}

export interface IResponseData {
  code: number;
  data: any;
  message: string;
}

interface IAnyMap { 
  [propName: string]: any;
}

exportinterface IRequestOptions { headers? : IHeaderConfig; signal? : AbortSignal; method? : EHttpMethods; query? : IAnyMap; params? : IAnyMap; data? : IAnyMap; body? : string; timeout? : number; credentials? :'include' | 'same-origin'; mode? :'cors' | 'same-origin'; cache? :'no-cache' | 'default' | 'force-cache';
}

/**
  * Http request
  * @param url request URL
  * @param options request options
  */interface IHttpInterface { request<T = IResponseData>(url: string, options? : IRequestOptions):Promise<T>;
}

const CAN_SEND_METHOD = ['POST'.'PUT'.'PATCH'.'DELETE'];

class Http implements IHttpInterface {
  public asyncrequest<T>(url: string, options? : IRequestOptions, abortController? : AbortController):Promise<T> {
    const opts: IRequestOptions = Object.assign({
      method: 'GET'.headers: {
        'Content-Type': 'application/x-www-form-urlencoded'.Accept: 'application/json'
      },
      credentials: 'include'.timeout: 10000.mode: 'cors'.cache: 'no-cache'
    }, options);

    abortController && (opts.signal = abortController.signal);

    if (opts && opts.query) {
      url += `${url.includes('? ')?'&' : '? '}${qs.stringify(
        filterObject(opts.query, Boolean),)}`;
    }

    const canSend = opts && opts.method && CAN_SEND_METHOD.includes(opts.method);

    if (canSend && opts.data) {
      opts.body = JSON.stringify(filterObject(opts.data, Boolean));
      opts.headers && Reflect.set(opts.headers, 'Content-Type'.'application/json');
    }

    console.log('Request Opts: ', opts);

    try {
      const res = await Promise.race([
        fetch(url, opts),
        new Promise<any>((_, reject) = > {
          setTimeout(() = > {
            return reject({ status: 408.statusText: 'Request timed out, please try again later', url }); }, opts.timeout); }));const result = await res.json();
      return result;
    } catch (e) {
      dealErrToast(e, abortController);
      returne; }}}const { request } = new Http();

export { request as default };
Copy the code

Advanced full version of useFetchData

Now that we’ve wrapped a FETCH based request library, we need to use it to write a useFetchData Hooks. Here’s a thought:

  • 1 – Keep it simple and uniform to use, minimizing the amount of request library logic code in the business component
  • 2 –hooksError handling is performed internally, and the component business layer does not need to care about handling
  • 3 – Returns response data, exception error, andloading

There is a above three goals, the next is to achieve, the specific code is as follows:

/** * /hooks/useFetchData.tsx */
import { useState, useEffect, useRef } from 'react';
import request, { IRequestOptions, IResponseData } from '.. /utils/request';

interface IFetchResData {
  data: T | undefined; 
  loading: boolean;
  error: any;
}

function useFetchData<T = any> (url: string, options? : IRequestOptions) :IFetchResData {
  // If it is a generic fetchData, any is useless. If it is a list, any can be replaced by the corresponding data paradigm
  const [data, setData] = useState<T>();
  const [loading, setLoading] = useState<boolean>(false);
  const [error, setError] = useState<any>(null);
  /** * Timeout or page destruction/route redirection, cancel the request */
  const abortControllerRef = useRef<AbortController>();

  function destory() {
    setData(undefined);
    setLoading(false);
    setError(null);
    abortControllerRef.current && abortControllerRef.current.abort();
  }

  useEffect(() = > {
    setLoading(true);
    abortControllerRef.current = new AbortController();
    request(url, options || {}, abortControllerRef.current).then(res= > {
      const { code, message, data } = res as IResponseData;
      if(code ! = =0) {
        console.log('Error Msg: ', message);
        throw new Error(message);
      }
      setData(data);
      setLoading(false);
    }).catch(err= > {
      setError(err);
    }).finally(() = > {
      setLoading(false);
    });

    return () = > destory();
  }, [url, JSON.stringify(options)]);

  return { loading, data, error };
}
 
export default useFetchData;

Copy the code

The code looks like this. Let’s see if it meets the four goals above:

1 – Simple and uniform to use, reducing request logic in business components

// 1 - Basic use GET without parameters
const { loading, data } = useFetchData('/user/list');

// 2 - Advanced uses GET with parameters
const [page, setPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number | undefined> (10);

const options = { query: { page, pageSize } };
const { loading, data } = useFetchData(getUserList, options);

// 3 - Advanced uses POST
const [page, setPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number | undefined> (10);

const options = useMemo(() = > ({
method: EHttpMethods.POST,
data: { page, pageSize }
}), [page, pageSize]);

const { loading, data } = useFetchData(postUserList, options);
Copy the code

As you can see, it is easy to use. You only need to pass the parameters and API address of the request to get the data and loading state. Also, the requested logic in the business component has only the necessary API URL and parameters, which are not reducible, and the rest of the logic is fully enclosed within hooks.

It is important to note that, because the second parameter of options (i.e., hooks) is an object, there is a problem that when the component repeats the request, it repeats the request, because each time options is a new object, even if it has not changed, it is a new memory address. So to avoid this, there are two solutions.

  • The first is the hooks layer, which I used hereuseEffect + JSON.stringifyTo make sure the parameters have changed before the request is rerequested.
  • Second: business layer solution, business code useuseMemoTo process.

Personally, I prefer the second method. At present, since it is the Demo stage, I have used both methods. You can use them as needed.

2-hooks internal error handling

Error handling is concentrated within the request library and hooks. Business components do not need to care about and handle errors, greatly reducing the complexity of business code. The core code is as follows:

// Request layer error handling
/** * error handling *@param err 
 * @param abortController 
 */
function dealErrToast(err: Error& ICustomRequestError, abortController? : AbortController) {
  switch(err.status) {
    case 408: {
      abortController && abortController.abort();
      (typeof window! = ='undefined') && message.error(err.statusText);
      break;
    }
    default: {
      console.log(err);
      break; }}}// Hooks layer error handling
const { code, message, data } = res as IResponseData;
if(code ! = =0) {
    // do something
    console.log('Error Msg: ', message);
    throw new Error(message);
}
Copy the code

Return response data and loading + TS enhanced data prompt

Each request returns the response data and loading state, again greatly simplifying the business code, very simple.

In this case, I added loading state to the hooks for clarity. In fact, the loading state can be aggregated from the data and error fields. However, I thought adding a loading would be clearer, so I wrote it this way.

The next benefit of TS is that if you set the type of data returned in the data model, then the data you get is a hint of data, which is very useful in business development, just in the editor. After a while, you can see the various properties of the returned data object, and the development is simply not too cool, no longer need to consult the API documentation while developing! The effect is as follows:

The specific code is as follows:

// export interface IUserStruct {id: number; name: string; age: number; } export interface IUserListResData { list: IUserStruct[]; Const {loading, data} = useFetchData<IUserListResData>(getLimitUserList, options);Copy the code

Your data will look like the figure above

conclusion

This article is also an article that I have wanted to summarize and write for a long time. After all, I really like to use FETCH, but few people use it in the company, so I think it is difficult to understand. This article says that the technical content is not what technical content, more should be experience sharing, more code, I suggest you Clone warehouse to run, I hope to be useful to you.