Location>code7788 >text

What you should know about hooks-style interface programming - useSWR

Popularity:608 ℃/2024-12-19 03:53:28

What is useSWR?

By its name we all know it's a React hooks, SWR is thestale-while-revalidateThe abbreviation of stale, stale means stale, revalidate means revalidate/revalidate, the combined sense can be understood as In the revalidation process, stale is used first, which in http requests means that the stale data cache is used first, and new data is requested to refresh the cache.

This is in the http requestCache-ControlThis is already implemented in the response header, e.g.

Cache-Control: max-age=60, stale-while-revalidate=3600

This means that with a cache expiration time of 60 seconds, when the cache expires and you request the interface and within 3600 of the cache expiration, the original expired cache will be used first as the result to return, and the server will be requested to refresh the cache.

Example:

In the case where swr is not used, replay the 304 negotiated cache request directly after the cache expires

In the case of swr, the cache expiration returns 200 expired cached data directly, and then a 304 negotiated cache request is made

However, implementing swr through a gateway layer such as nginx doesn't allow for precise control of interface caching, and even if therevalidatelatterfreshThe data is returned, and there's no way to get the page to re-render, so you have to wait for the next interface request.

useSWRImplemented http request SWR caching directly in the front-end code layer.

Usage

In the traditional model, we would write a data request in such a way that we need to manage the data request by defining multiple states and making imperative interface calls in side effects.

import { useEffect, useState } from "react";
import Users from "./Users";

export default function App() {
  const [users, setUsers] = useState([]);
  const [isLoading, setLoading] = useState(false);

  const getUsers = () => {
    setLoading(true);
    fetch("/api/getUsers")
      .then((res) => ())
      .then((data) => {
         setUsers(data);
      })
      .finally(() => {
         setLoading(false)
      })
  }

  useEffect(() => {
    getUsers();
  }, []);


  return (
    <div>
      {isLoading && <h2>loading... </h2>}
      <UserList users={users} />
    </div>
  );
}

With useSWR, we only need to tell SWR the unique key of the request, and how to handle it.fetcher method, which is automatically requested after the component is mounted

import useSWR from "swr";
import Users from "./Users";

const fetcher = (...args) => fetch(...args).then((res) => ())

export default function App() {
  const { data: users, isLoading, mutate } = useSWR('/api/getUsers', fetcher)

  return (
    <div>
      {isLoading && <h2>loading... </h2>}
      <UserList users={users} />
    </div>
  );
}

Incoming parameters for useSWR

  • key: a unique key for the request, which can be a string, function, array, object, etc.
  • fetcher:(Optional) A Promise return function that requests data.
  • options:(Optional) option object for this SWR hook

key will be passed as an input parameter to thefetcher 函数, Generally it can be a request forURLact askey。Can be customized according to the scenario key formats,Let's say I have additional request parameters,Then put key Defined as an array ['/api/getUsers', { pageNum: 1 }], the SWR automatically serializes the key value internally for cache matching.

Returns from useSWR

  • data:: Adoptionfetcher The result of the processed request, before it is returned, isundefined

  • error: fetcher thrown error

  • isLoading: Whether there is a request in progress and no "loaded data" at the moment.

  • isValidating: whether there is a request or revalidation load

  • mutate(data?, options?): Functions to change cached data

crux

Global caching mechanism

We use the key every time we use the SWR, and this will be used as a unique identifier to store the result in the global cache, this kind ofDefault Cachebehavior is actually very useful.

For example, getting a list of users is a very frequent request in our product, and there are many interfaces to break down the list of users

When we write the requirements, we may not know whether the interface data has been stored in the redux, and to put the data in the redux is a relatively troublesome operation has a management cost, then most people's approach is that there is a place to use, I'll re-request it again.

For example, if there is a list of users in a modal box that has to be requested every time it is opened (with remote search), and the user has to wait every time, you can of course elevate the state of the user list outside of the modal box, but there is a trade-off, and the external parent component doesn't really care about the state of the user list at all.

Request status differentiation

When the first request is made, i.e., when no cache corresponding to the key is found, then the request is immediately initiated.isLoading together withisValidating are true.

When the second request is made with a cache, then take the cached data and render it first, and then make the request, theisValidating is true.

That means as long as the request is in progress, it'sisValidating, only if there is no cached data and it is being requested.isLoadingStatus.

Corresponding state diagrams:

file

In the above cases, the key is a fixed value, but in many more scenarios, the key value will change due to changes in the request parameters.

For example, a search user's key is defined like this

const [search, setSearch] = useState('');
const { data } = useSWR(['/api/users', search], fetcher)

Each input will cause the key to change, the key change default will re-request the interface, but in fact, the key change also means that the data is not trustworthy, you need to reset the data inside, so thedata will be immediately reset toundfined If the new key already has a cached value, it will be rendered first. If the new key already has a cached value, the cached value is also rendered first.

Then we actually added almost no extra code and implemented a user search function with its own data cache.

The state diagram when the corresponding key changes:

file

What if we preferred to keep the data before the key change and show it first? Because we'd still see the short-livedno-data

We mainly add configuration to the third parameter, options.keepPreviousData can be realized

file

The implementation is actually the same as when we search for branches in gitlab

Key changes and retains the state diagram of the data:

file

Linkage requests and manual triggers

In many cases interface requests are dependent on the result of another interface request, or the request is initiated only under certain circumstances.

First of all there are three ways on how to make the component mount without requesting it

Configuration item implementation

set upoptions parametersrevalidateOnMount , 这种方法如果已有缓存数据,It will still take the cached data and render it

dependency model

Returns when key is givenfalsy Value or Provide a function and throw an error

const { data } = useSWR(isMounted ? '/api/users' : null, fetcher)

const { data } = useSWR(() => isMounted ? '/api/users' : null, fetcher)

// throw an error
const { data: userInfo } = useSWR('/api/userInfo')
const { data } = useSWR(() => '/api/users?uid=' + , fetcher)

Then we realize a business scenario: data source-database-data table linkage request

We have implemented linkage requests in an almost automated way.

The following is a code example:

const DependenceDataSource = () => {
    const [form] = ();
    const dataSourceId = ("dataSourceId", form);
    const dbId = ("dbId", form);

    const { data: dataSourceList = [], isValidating: isDataSourceFetching } =
        useSWR({ url: "/getDataSource" }, dataSourceFetcher);
        
    const { data: dbList = [], isValidating: isDatabaseFetching } = useSWR(
        () =>
            dataSourceId
                ? { url: "/getDatabase", params: { dataSourceId } }
                : null,
        databaseFetcher
    );

    const { data: tableList = [], isValidating: isTableFetching } = useSWR(
        () =>
            dataSourceId && dbId
                ? { url: "/getTable", params: { dataSourceId, dbId } }
                : null,
        tableFetcher
    );

    return (
        <Form
            form={form}
            style={{width: 400}}
            layout="vertical"
            onValuesChange={(changedValue) => {
                if ("dataSourceId" in changedValue) {
                    (["dbId", "tableId"]);
                }
                if ("dbId" in changedValue) {
                    (["tableId"]);
                }
            }}
        >
            < name="dataSourceId" label="data sources">
                <Select
                    placeholder="请选择data sources"
                    options={dataSourceList}
                    loading={isDataSourceFetching}
                    allowClear
                />
            </>
            < name="dbId" label="comprehensive database">
                <Select
                    placeholder="请选择comprehensive database"
                    options={dbList}
                    loading={isDatabaseFetching}
                    allowClear
                />
            </>
            < name="tableId" label="data sheet">
                <Select
                    placeholder="请选择data sheet"
                    options={tableList}
                    loading={isTableFetching}
                    allowClear
                />
            </>
        </Form>
    );
};

Adoption of manual gear mode

Using this method above takes advantage of the fact that key changes automaticallyrevalidateThe mechanism of the data realizes the linkage, but there is a very bigmalpracticeYou need to extract all the dependencies from the key into thestateEnables components to be re-rendered forrevalidate. Kind of forces you to use controlled mode, which can cause performance issues.

So we need to utilizemutateMake a manual request.mutate(key?, data, options)

You can introduce it directly from the swr globalmutatemethod, you can also use themutate Methods.

Distinction:

  • security situationmutateAdditional key required
  • Within hooksmutateBinds the key directly
// global use
import { mutate } from "swr"
function App() {
  mutate(key, data, options)
}

// hookutilization
const UsersMutate = () => {
    const { data, mutate } = useSWR({ url: "/getNewUsers" }, fetcher, {
        revalidateOnFocus: false,
        dedupingInterval: 0,
        revalidateOnMount: false
    });

    return (
        <div>
            <
                onSearch={(value) => {
                    mutate([{ id: 3, name: "user_" + value }]);
                }}
            />
            <List style={{ width: 300 }}>
                {data?.map((user) => (
                    < key={}>{}</>
                ))}
            </List>
        </div>
    );
}

mutate will immediately use the incomingdata Update the cache, and then it will do it againrevalidate Data Refresh

Use the globalmutatePass in the key{ url: "/getNewUsers" } can achieve the same effect and use the globalmutate

If the key passed into mutate is a function, you can batch clear the cache. Note: In mutate, the key passed into a function meansfilter functionThis is not the same as passing in the key function in useSWR.

mutate(
    (key) => typeof key === 'object' &&  === getUserAPI &&  !== '',
    undefined,
  {
    revalidate: false
  }
);

However, we can note that the now passed-inkeyis without request parameters, the hooks in themutateIt is also not possible to modify the boundkey值,那么怎么携带请求参数呢?

useSWRMutation

useSWRMutationis a manual mode SWR that can only be used by returning thetriggermethod for data updates.

That means:

  1. It does not automatically use cached data
  2. It does not automatically write to the cache (the default behavior can be modified via configuration)
  3. Does not automatically request data when the component is mounted or when the key changes

It returns a slightly different function:

const { data, isMutating, trigger, reset, error } = useSWRMutation(
    key,
    fetcher, // fetcher(key, { arg })
    options
);

trigger('xxx')

useSWRMutation (used form a nominal expression)fetcherThe function can additionally pass aargparameter, in thetriggercan be passed in, so let's realize that the2.1 Dependency modelThe dependency linkage request in the

  1. Define three fetchers to receive parameters, which are not necessarily designed as{ arg }formality
const dataSourceFetcher = (key) => {
    return new Promise<any[]>((resolve) => {
        request(key).then((res) => resolve(res))
    });
};

const databaseFetcher = (key, { arg }: { arg: DatabaseParams }) => {
    return new Promise<any[]>((resolve) => {
        const { dataSourceId } = arg;
        if (!dataSourceId) return resolve([])
        request(key, { dataSourceId }).then((res) => resolve(res))
    });
};

const tableFetcher = (key, { arg }: { arg: TableParams }) => {
    return new Promise<any[]>((resolve) => {
        const { dataSourceId, dbId } = arg;
        if (!dataSourceId || !dbId) return resolve([])
        request(key, { dataSourceId, dbId }).then((res) => resolve(res))
    });
};
  1. Defining hooks
const { data: dataSourceList = [], isValidating: isDataSourceFetching } =
    useSWR({ url: "/getDataSource" }, dataSourceFetcher);

const { data: dbList = [], isMutating: isDatabaseFetching, trigger: getDatabase, reset: clearDatabase } = useSWRMutation(
    { url: "/getDatabase" },
    databaseFetcher,
);

const { data: tableList = [], isMutating: isTableFetching, trigger: getTable, reset: clearTable } = useSWRMutation(
    { url: "/getTable" },
    tableFetcher
);
  1. manual trigger
<Form
    onValuesChange={(changedValue) => {
        if ("dataSourceId" in changedValue) {
            (["dbId", "tableId"]);
            clearDatabase();
            clearTable();
            getDatabase({ dataSourceId: });
        }
        if ("dbId" in changedValue) {
            (["tableId"]);
            clearTable();
            getTable({
                dataSourceId: ("dataSourceId"),
                dbId: ,
            });
        }
    }}
>
    // FormItemsummarize
</Form>

No Cache Write

file

However, the use ofuseSWRMutationThis way, if the library table also has a remote data search, you can't use the caching feature.

performance optimization

useSWR was designed with performance in mind.

  • Self-throttling
    When we call the same interface multiple times in a short period of time, only one request will be triggered. If multiple user components are rendered at the same time, it will trigger therevalidatemechanism, but it will only actually be triggered once. This time throttling time is determined by thededupingIntervalConfiguration control, which defaults to2000ms

A Question, How do I implement stabilization? No configurations available

  • revalidate empressfreshDatatogether withstaleDataThe comparison between the two is deep to avoid unnecessary rendering, see thedequal

  • Dependent collection
    If there is no consumptionhooksreturned, then the state change will not result in a re-rendering of the

const { data } = useSWR('xxx', fetcher);

// render only when data changes, isValidating, isLoading won't trigger rendering since it doesn't introduce timely changes

The implementation of dependency collection is clever

  1. Define a ref for dependency collection, no dependencies by default
    file

  2. pass (a bill or inspection etc)getAdd after realization visit
    file

  3. due tostateChanges will definitely result in rendering, so all of this state is controlled by theuseSyncExternalStoremanagerial
    file

  4. Rendering is triggered only if unequal, and if not in the stateDependencies collection, the direct
    file

summarize

useSWR can greatly improve the user experience, but in actual use, may still need to leave a little thought combined with the business to see whether to use the caching features, such as some of the submission business scenarios on the library table in real time is very high, then you should consider whether to have useSWR.

Furthermore, the number stack products in the actual development of the business data will rarely be encapsulated in hooks, such as the user list can be encapsulated into theuseUserListForm UseuseListI'm not sure if I'm going to be able to do that. Feel more is the reason for the development habits, feel that in the future they may not reuse will not do too much encapsulation, imperative programming a shuttle.

ultimate

Welcome to [Kangaroo Cloud Digital Stack UED Team]~
Kangaroo Cloud Digital Stack UED team continues to share the results of technology for the majority of developers, successively participated in the open source welcome star

  • Big Data Distributed Task Scheduler - Taier
  • Lightweight Web IDE UI framework - Molecule
  • SQL Parser Project for Big Data - dt-sql-parser
  • Kangaroo Cloud Digital Stack front-end team code review engineering practices document - code-review-practices
  • A faster, more flexible and easier to use module packager - ko
  • A component testing library for antd - ant-design-testing