Before the words

Business data endows native components with the ability to provide services externally. Business components can obtain static data and dynamic data, while business components obtain dynamic data through data requests.

Writing a “high availability” business system depends on writing the business components. “Fetch” is the most common scenario of service components. Newcomers often fall into the mistake of “redundancy” when writing fetch logic, which causes a series of problems that are difficult to maintain.

I’ll show you how to use hooks to optimize the fetch logic in the React project.

The body of the

1. Common data requests

Business components typically make ordinary data requests and simply render the data they get, using the following logic:

  1. Initialize data service data and loading state value
  2. Mounted Phase The MOUNTED phase obtains service data and synchronizes the loading state during the request
  3. Render data service data according to the loading state

Example code is as follows:

import React, { useEffect, useState } from "react";
import { Spin } from "@arco-design/web-react";

const Cmpt: React.FC = () = > {
  // 1. Initialize data service data and loading state value
  const [data, setData] = useState("");
  const [loading, setLoading] = useState(false);

  // 2. Mounted A request is launched to obtain service data. The Loading state is synchronized during the request
  useEffect(() = > {
    setLoading(true);
    fetchData()
      .then(setData)
      .finally(() = > setLoading(false)); } []);// 3. Render service data according to the loading state
  return <Spin loading={loading}>{data}</Spin>;
};

export default Cmpt;
Copy the code

This approach is maintainable, of course, without any problems, and fulfills the business requirements.

However, it is annoying to write loading and data variables repeatedly. We can separate this data request from the status update logic into a common data request Hook. The implementation is as follows:

import React from 'react';
import { debounce } from 'lodash';

export interface IBuildUseFetchOptions<T, P> {
  /** Indicates whether to query immediately. The default value is true */immediate? :boolean;
  /** Buffeting interval (ms). The default value is 300 */duration? :number;
  /** Associative property, default is empty array */relation? :Array<string>;
  /** Filter criteria, default is empty array */properties? :Array<string | { key: string; value: any} >./** Filter the conditional conversion hook function */getQuery? :(query: any, props: P) = > any;
  /** Load the data hook function */getData? :(query: any, props: P) = > Promise<T>;
  /** Custom error logic */onError? :(err: any) = > void;
}

/** * Data request Hook factory function *@param Options data request Hook configuration *@returns* /
export function buildUseFetch<T.P = {}>(options: IBuildUseFetchOptions<T, P>) {
  const {
    immediate = true,
    duration = 200,
    relation = [],
    properties = [],
    getQuery = query= > query,
    getData = () = > Promise.resolve(undefined),
    onError = err= > console.error(err),
  } = options;

  return function useFetch<C = {}>(props: P, defaultQuery? : C) {const [inited, setInited] = React.useState(false);
    const [loading, setLoading] = React.useState(immediate);

    const [data, setData] = React.useState<T>();

    const[query, setQuery] = React.useState({ ... properties.reduce((p, c) = > {
        if (typeof c === 'object') {
          p[c.key] = c.value;
        } else {
          p[c] = undefined;
        }
        return p;
      }, {} as any),
      ...(defaultQuery || {}),
    });

    // Cache variables
    const ref = React.useRef({
      props,
      inited,
      loading,
      query,
      data,
      isUnmounted: false});// Data request method
    const onFetch = React.useCallback(
      debounce(_query= > {
        setTimeout(async() = > {try {
            constnewQuery = getQuery( { ... _query, }, ref.current.props );const targetQuery = Object.keys(newQuery).reduce((p, c) = > {
              const v = newQuery[c];
              if(v ! = =undefined&& v ! = =null) {
                p[c] = v;
              }
              return p;
            }, {} as any);

            const _data = awaitgetData(targetQuery, ref.current.props); ! ref.current.isUnmounted && setData(_data); ref.current.data = _data; }catch (err) {
            onError(err);
          } finally {
            !ref.current.isUnmounted && setLoading(false);
            ref.current.loading = false; }}); }, duration), [] );// Data loading method
    const onLoad = React.useCallback(_query= > {
      setLoading(true);
      ref.current.loading = true; onFetch(_query); } []);// Data refresh method
    const onRefresh = React.useCallback(() = >{ onLoad(ref.current.query); } []);// Monitor the change of query parameters during component update, and automatically execute data loading method if the change occurs
    React.useEffect(() = > {
      if(! ref.current.inited) {return;
      }
      ref.current.query = query;
      onRefresh();
    }, [query]);

    // Monitor component Props changes during component updates (filtered by association attribute), and perform data loading automatically if the changes occur
    React.useEffect(() = > {
      if(! ref.current.inited) {return;
      }
      const oldProps = ref.current.props;
      ref.current.props = props;
      if (relation.find(p= > (oldProps as any)[p] ! == (propsas any)[p])) {
        onRefresh();
      }
    }, [props]);

    // Determine whether the data loading method is performed automatically when the component is initialized
    React.useEffect(() = > {
      setInited(true);
      ref.current.inited = true;

      if (immediate) {
        onLoad(ref.current.query);
      }

      return () = > {
        ref.current.isUnmounted = true; }; } []);const fetchResult = {
      inited,
      loading,
      query,
      data,
    };

    const fetchAction = {
      setInited,
      setLoading,
      setQuery,
      setData,
      onLoad,
      onRefresh,
    };

    const ret: [typeof fetchResult, typeof fetchAction] = [
      fetchResult,
      fetchAction,
    ];

    return ret;
  };
}
Copy the code

To demonstrate the following scene here directly put perfect Hook, the source code has been uploaded to Github, there may be students can kangkangha. The address is here: github.com/pwcong/fron…

We can use this data to request Hook to optimize the previous cumbersome code, and the optimized result is as follows:

import React from "react";
import { Spin } from "@arco-design/web-react";

// Generate Hook functions from factory functions for easy reuse
const useFetch = buildUseFetch({
  // Data request logic
  getData: fetchData,
});

const Cmpt: React.FC = (props) = > {
  // Use data to request a Hook
  const [{ loading, data }] = useFetch(props);

  return <Spin loading={loading}>{data}</Spin>;
};

export default Cmpt;
Copy the code

The Hook named “useFetch” is far more capable than that. Here are a series of demos to demonstrate how to use it to break into various business scenarios one by one.

1.1 Custom request parameters and parameter conversion

In the most common scenario in a service system, click the “View button” to jump to the details page. At this time, you need to obtain the “path parameters” to initiate the “request for data details”.

UseFetch supports custom request parameters to handle this request scenario, with the following example code:

import React from "react";
import { useParams } from "react-router";

const useFetch = buildUseFetch({
  // Data request logic
  getData: (query) = > fetchData(query.id),
});

const Cmpt: React.FC = (props) = > {
  // Get route parameters
  const { id } = useParams<{ id: string} > ();// Pass in the request parameters
  const [{ data }] = useFetch(props, {
    id,
  });

  / /... slightly
};

export default Cmpt;
Copy the code

If the request parameters need to be converted, useFetch also supports the “parameter conversion” logic to be defined in the “build” process, as follows:

import React from "react";
import { useParams } from "react-router";

const useFetch = buildUseFetch({
  // Request parameter conversion logic
  getQuery: ({ id }) = > {
    return {
      id,
      timestamp: new Date().getTime(),
    };
  },
  // Data request logic
  getData: ({ id, timestamp }) = > fetchData(id, timestamp),
});

const Cmpt: React.FC = (props) = > {
  // Get route parameters
  const params = useParams<{ id: string} > ();// Pass in the request parameters
  const [{ data }] = useFetch(props, params);

  / /... slightly
};

export default Cmpt;
Copy the code

1.2 Manually Triggering a Request

There is usually a scenario in which a component does not initiate a request by itself and then initiates a request after the user performs an action to confirm the request parameters.

By default, useFetch sends a request immediately. However, it supports not sending a request immediately during Build. The example code is as follows:

import React from "react";
import { Button } from "@arco-design/web-react";

const useFetch = buildUseFetch({
  // Do not request immediately
  immediate: false.// Data request logic
  getData: fetchData,
});

const Cmpt: React.FC = (props) = > {
  const [{ data }, { onLoad }] = useFetch(props);

  return (
    <>{/ *... */ {data} {/* User triggered data request */<Button onClick={()= > onLoad({ id: "a" })}>A</Button>
      <Button onClick={()= > onLoad({ id: "b" })}>B</Button>
    </>
  );
};

export default Cmpt;
Copy the code

If you look closely at the implementation of the data Hook, you should see that an “onRefresh” action interface is exported along with “onLoad”, which serves as the name “refresh request”.

Data Hook each incoming request parameter is cached as the final request parameter, so if you want to reuse the parameters of the previous request to re-initiate a data request, you can use the “onRefresh” code as follows:

/ /... slightly

  const [{ data }, { onRefresh }] = useFetch(props);
  
  return (
    <>{/ *... */ {data} {/* retrigger data request */<Button onClick={onRefresh}>Refresh</Button>
    </>
  );

/ /... slightly
Copy the code

1.3 Listening for component property changes

There is a business component that has a property change that requires the request to be re-initiated as a request parameter and the result of the request to be rendered.

Readers who have used Vue development will love its “Watch” capabilities, and React can do the same.

Strictly speaking, React is not a direct “watch”, but a change of “props” or “state” triggers an “Update”. We compare the “new props or state” with the “old props or state” to determine whether an attribute or state is changed and perform an action. Achieve the effect of Watch.

Therefore, for this scenario, sample code for the use of “useFetch” is as follows:

import React from "react";

type IProps = {
  id: string;
};

const useFetch = buildUseFetch<unknown, IProps>({
  // Set the name of the listening attribute. The change of the listening attribute will trigger a new request
  relation: ["id"].// Use attributes as request parameters
  getData: (_, props) = > fetchData(props.id),
});

const Cmpt: React.FC<IProps> = (props) = > {
  const [{ data }] = useFetch(props);

  return <>{data}</>;
};

export default Cmpt;
Copy the code

1.4 summary

UseFetch uses anti-shake to implement requests. By default, the anti-shake interval is 200 milliseconds, which can be customized by users.

Here are three more common data request scenarios that cover the vast majority of real business, and more capable readers can copy the source code to explore

2. List data request

Tabular data request is a common request scenario subdivided by data request, and the page flow is similar to the common data request flow. A list request is not requested in the Mounted phase. For example:

  • Provides list-related page properties (current page number, total page number, next page, etc.)
  • Provides interface for list-related operations (refresh, next page, etc.)
  • Associated query criteria (changes to query criteria trigger re-requests)

Example code is as follows:

import React, { useEffect, useState } from "react";
import { Table, Input } from "@arco-design/web-react";

const Cmpt: React.FC = () = > {
  // Initializes the list-related state
  const [data, setData] = useState([]);
  const [query, setQuery] = useState<Record<string.any> > ({pageNo: 1.pageSize: 10});const [total, setTotal] = useState(0);
  const [loading, setLoading] = useState(false);

  // List data request method
  const fetchData = (query: Record<string.any>) = > {
    setQuery(query);
    setLoading(true);

    fetchData(query)
      .then((res) = > {
        setData(res.data);
        setTotal(res.total);
      })
      .finally(() = > setLoading(false));
  };

  // mounted Initializes list data
  useEffect(() = >{ fetchData(query); } []);return (
    <>
      <Input
        onChange={(v)= >Const newQuery = {... query, keyword: v }; fetchData(newQuery); }} / ><Table
        loading={loading}
        data={data}
        pagination={{
          current: query.pageNo.pageSize: query.pageSize.total.onChange: (current.size) = >FetchData ({pageNo: current, pageSize: size,}); }}} / ></>
  );
};

export default Cmpt;
Copy the code

In line with the normal data request optimization method, we can separate the data request from the status update logic into a common list request Hook, as follows:

import React from 'react';
import { debounce, omit } from 'lodash';

/** Platform id */
export enum EListPlatform {
  /** Desktop */
  'Desktop' = 'Desktop'./** mobile */
  'Mobile' = 'Mobile',}export type IUseListData<T> = Record<string, unknown> & {
  / * * * / data
  data: Array<T>;
  /** Total data */
  totalSize: number;
};

export type IUseListQuery = {
  /** Page number */
  pageNo: number;
  /** Paging size */
  pageSize: number;
};

export interface IBuildUseListOptions<T, P> {
  /** Platform id. The default value is Desktop */platform? : EListPlatform;/** Indicates whether to query immediately. The default value is true */immediate? :boolean;
  /** Buffeting interval (ms). The default value is 300 */duration? :number;
  /** Associative property, default is empty array */relation? :Array<string>;
  /** Filter criteria, default is empty array */properties? :Array<string | { key: string; value: any} >./** Filter the conditional conversion hook function */getQuery? :(query: any, props: P) = > any;
  /** Load the data hook function */getData? :(query: any, props: P) = > Promise<IUseListData<T>>;
  /** Custom error logic */onError? :(err: any) = > void;
}

/** * list requests Hook factory function *@param Options list requests Hook configuration *@returns* /
export function buildUseList<T.P = {}>(options: IBuildUseListOptions<T, P>) {
  const {
    // platform = EListPlatform.Desktop,
    immediate = true,
    duration = 200,
    relation = [],
    properties = [],
    getQuery = query= > query,
    getData = () = > Promise.resolve({ data: [].totalSize: 0 }),
    onError = err= > console.error(err),
  } = options;

  return function useList<C = {}>( props: P, _defaultQuery? : C & Partial<IUseListQuery> ) {const [inited, setInited] = React.useState(false);
    const [loading, setLoading] = React.useState(immediate);
    const [loadingMore, setLoadingMore] = React.useState(false);

    const defaultQuery = React.useMemo(
      () = > ({
        pageNo: 1.pageSize: 10. _defaultQuery, }), [] );const [pageNo, setPageNo] = React.useState(defaultQuery.pageNo);
    const [pageSize, setPageSize] = React.useState(defaultQuery.pageSize);
    const [totalSize, setTotalSize] = React.useState(0);

    const [list, setList] = React.useState<Array<T>>([]);
    const [data, setData] = React.useState<IUseListData<T>>({
      data: list,
      totalSize: totalSize,
    });

    const[query, setQuery] = React.useState({ ... properties.reduce((p, c) = > {
        if (typeof c === 'object') {
          p[c.key] = c.value;
        } else {
          p[c] = undefined;
        }
        return p;
      }, {} as any),
      ...omit(defaultQuery, ['pageNo'.'pageSize'])});// Whether to allow more loading
    const hasMore = React.useMemo(
      () = > pageSize * pageNo < totalSize && list.length < totalSize,
      [pageSize, pageNo, list, totalSize]
    );

    // Cache variables
    const ref = React.useRef({
      props,
      pageNo,
      pageSize,
      totalSize,
      inited,
      loading,
      loadingMore,
      query,
      data,
      list,
      isUnmounted: false});// Load the state change method
    const changeLoading = React.useCallback(
      (active? :boolean, more? :boolean) = > {
        if (active) {
          setLoading(true);
          ref.current.loading = true;
          if (more) {
            setLoadingMore(true);
            ref.current.loadingMore = true; }}else {
          setLoading(false);
          ref.current.loading = false;
          setLoadingMore(false);
          ref.current.loadingMore = false; }} []);// Data request method
    const onFetch = React.useCallback(
      debounce(_query= > {
        setTimeout(async() = > {try {
            // Get the transformed request criteria by filtering the criteria transform hook function
            const newQuery = getQuery(
              {
                pageNo: ref.current.pageNo,
                pageSize: ref.current.pageSize, ... _query, }, ref.current.props );// Filter request conditions whose value is undefined or null
            const targetQuery = Object.keys(newQuery).reduce((p, c) = > {
              const v = newQuery[c];
              if(v ! = =undefined&& v ! = =null) {
                p[c] = v;
              }
              return p;
            }, {} as any);

            const result = awaitgetData(targetQuery, ref.current.props); ! ref.current.isUnmounted && setData(result); ref.current.data = result;const { data = [], totalSize: _totalSize = 0} = result; ! ref.current.isUnmounted && setTotalSize(_totalSize); ref.current.totalSize = _totalSize;let _list: Array<T> = [];
            if (ref.current.loadingMore) {
              _list = ref.current.list.concat(data);
            } else {
              _list = data;
            }
            !ref.current.isUnmounted && setList(_list);
            ref.current.list = _list;
          } catch (err) {
            onError(err);
          } finally {
            !ref.current.isUnmounted && changeLoading(false); }}); }, duration), [] );// Data loading method
    const onLoad = React.useCallback(
      (_query, _options? : { more? :boolean }) = > {
        changeLoading(true, _options? .more); onFetch(_query); } []);// Data refresh method
    const onRefresh = React.useCallback((reload? :boolean) = > {
      const _reload = reload === undefined ? true : reload;
      if (_reload) {
        setPageNo(1);
        ref.current.pageNo = 1; } onLoad(ref.current.query); } []);// More methods to load data
    const onLoadMore = React.useCallback(
      debounce(() = > {
        changeLoading(true.true);

        setPageNo(ref.current.pageNo + 1);
        ref.current.pageNo++;
      }, duration),
      []
    );

    // Monitor page number change when component update, if change, automatic data loading method
    React.useEffect(() = > {
      if(! ref.current.inited) {return;
      }
      ref.current.pageNo = pageNo;
      onLoad(ref.current.query);
    }, [pageNo]);

    // Monitor page count and query parameter change when component update, if change, automatically execute data loading method
    React.useEffect(() = > {
      if(! ref.current.inited) {return;
      }
      ref.current.pageSize = pageSize;
      ref.current.query = query;
      onRefresh(true);
    }, [pageSize, query]);

    // Monitor component Props changes during component updates (filtered by association attribute), and perform data loading automatically if the changes occur
    React.useEffect(() = > {
      if(! ref.current.inited) {return;
      }
      const oldProps = ref.current.props;
      ref.current.props = props;
      if (relation.find(p= > (oldProps as any)[p] ! == (propsas any)[p])) {
        onRefresh(true);
      }
    }, [props]);

    // Determine whether the data loading method is performed automatically when the component is initialized
    React.useEffect(() = > {
      setInited(true);
      ref.current.inited = true;

      if (immediate) {
        onLoad(ref.current.query);
      }

      return () = > {
        ref.current.isUnmounted = true; }; } []);const listResult = {
      inited,
      loading,
      loadingMore,
      pageNo,
      pageSize,
      totalSize,
      hasMore,
      query,
      list,
      data,
    };
    const listAction = {
      setInited,
      setLoading,
      setLoadingMore,
      setPageNo,
      setPageSize,
      setTotalSize,
      setQuery,
      setList,
      setData,
      onLoad,
      onRefresh,
      onLoadMore,
    };

    const ret: [typeof listResult, typeof listAction] = [
      listResult,
      listAction,
    ];

    return ret;
  };
}
Copy the code

To demonstrate the following scene here also directly put perfect Hook, the source has been uploaded to Github, there may be students can kangkangha. The address is here: github.com/pwcong/fron…

We can use this list to request a Hook to optimize the previously cumbersome code, which results in the following:

import React from "react";
import { Table, Input } from "@arco-design/web-react";

const useList = buildUseList({
  getData: fetchData,
});

const Cmpt: React.FC = (props) = > {
  // Initializes the list-related state
  const [
    { loading, query, list, pageNo, pageSize, total },
    { setPageNo, setPageSize, setQuery },
  ] = useList(props);

  return (
    <>
      <Input
        onChange={(v)= >Const newQuery = {... const newQuery = {... query, keyword: v }; setQuery(newQuery); }} / ><Table
        loading={loading}
        data={list}
        pagination={{
          current: pageNo.pageSize: pageSize.total.onChange: (current.size) = >{// The query condition changes automatically trigger a new request setPageNo(current); setPageSize(size); }}} / ></>
  );
};

export default Cmpt;
Copy the code

The implementation of “useList” refers to “useFetch” and adapates it to “list scenarios”, so it is the same in terms of capabilities, except list-related capabilities.

The biggest feature is that the useList is associated with query conditions. The change of query conditions will trigger a new request. The query conditions include pageNo, pageSize and Query.

So setPageNo, setPageSize, and setQuery all trigger data requests.

2.1 List refresh Request

Data or list operations are performed in the following scenarios:

  • Data operations include “Edit” data. After such operations are successful, you need to refresh the list with the page number unchanged.
  • Common list operations include adding data. After such operations are successful, you need to refresh the list and set the page number to the home page.

“UseList” provides the “onRefresh” operation interface, which receives a “Boolean” parameter (default value is “true”). If it is “true”, “set the page number to the first page and initiate a request”, otherwise “initiate a request only”, example code is as follows:

import React from "react";
import { Button } from "@arco-design/web-react";

const useList = buildUseList({
  getData: fetchData,
});

const Cmpt: React.FC = (props) = > {
  // Initializes the list-related state
  const [, { onRefresh }] = useList(props);

  return (
    <>{/ * *... Just * /}<Button onClick={()= >OnRefresh (true)} > refresh</Button>
    </>
  );
};

export default Cmpt;
Copy the code

2.2 Mobile List Request

Mobile lists have one more scenario than desktop lists, where the list data request is “load more” instead of “next page.”

There are also “load more” requirements on the desktop, but they are less common.

Note that “list data” increases the page number of the request parameter under the “load more” action, and the list data is continuously concatenated rather than replacing the original list data directly.

In the Build phase, useList can set the platform parameter (Desktop by default) to Mobile to support this scenario. The example code is as follows:

import React from "react";
import { Button } from "@arco-design/web-react";

const useList = buildUseList({
  // Set the application scenario to mobile
  platform: EListPlatform.Mobile,
  // Data request logic
  getData: fetchData,
});

const Cmpt: React.FC = (props) = > {
  // Initializes the list-related state
  const [{ list, pageNo }, { onLoadMore }] = useList(props);

  return (
    <>{/ * *... */ ** load more */<Button onClick={()= >OnLoadMore ()}> Current page: {pageNo}, load more</Button>
    </>
  );
};

export default Cmpt;
Copy the code

The last

Component data requests are one of the most important scenarios in a business system, so I included them in the React Common Solutions column. The above considerations and solutions have been applied stably in real projects.

Of course, this article is only as the author’s own understanding, if the mistake is still looking for you to point out or provide better advice for reference modification ha ~