What is useSWR?
By its name we all know it's a React hooks, SWR is thestale-while-revalidate
The 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-Control
This 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 therevalidate
latterfresh
The 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.
useSWR
Implemented 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.isLoading
Status.
Corresponding state diagrams:
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:
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
The implementation is actually the same as when we search for branches in gitlab
Key changes and retains the state diagram of the data:
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 automaticallyrevalidate
The mechanism of the data realizes the linkage, but there is a very bigmalpractice
You need to extract all the dependencies from the key into thestate
Enables components to be re-rendered forrevalidate
. Kind of forces you to use controlled mode, which can cause performance issues.
So we need to utilizemutate
Make a manual request.mutate(key?, data, options)
,
You can introduce it directly from the swr globalmutate
method, you can also use themutate
Methods.
Distinction:
- security situation
mutate
Additional key required - Within hooks
mutate
Binds 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 globalmutate
Pass 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 function
This 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-inkey
is without request parameters, the hooks in themutate
It is also not possible to modify the boundkey
值,那么怎么携带请求参数呢?
useSWRMutation
useSWRMutation
is a manual mode SWR that can only be used by returning thetrigger
method for data updates.
That means:
- It does not automatically use cached data
- It does not automatically write to the cache (the default behavior can be modified via configuration)
- 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)fetcher
The function can additionally pass aarg
parameter, in thetrigger
can be passed in, so let's realize that the2.1 Dependency model
The dependency linkage request in the
- 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))
});
};
- 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
);
- 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
However, the use ofuseSWRMutation
This 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 therevalidate
mechanism, but it will only actually be triggered once. This time throttling time is determined by thededupingInterval
Configuration control, which defaults to2000ms
。
A Question, How do I implement stabilization? No configurations available
-
revalidate
empressfreshData
together withstaleData
The comparison between the two is deep to avoid unnecessary rendering, see thedequal。 -
Dependent collection
If there is no consumptionhooks
returned, 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
-
Define a ref for dependency collection, no dependencies by default
-
pass (a bill or inspection etc)
get
Add after realization visit -
due to
state
Changes will definitely result in rendering, so all of this state is controlled by theuseSyncExternalStore
managerial -
Rendering is triggered only if unequal, and if not in the stateDependencies collection, the direct
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 theuseUserList
Form UseuseList
I'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