May 31, 2021 Article blog
The article comes from the public number: front-end bottle king
Swr is a hook component that can be used as a request library and a state management library, and this article focuses on how to use swr in a project and parses the principles of swr. Read swr's source code from the principle
- What is swr
- Swr's source code
useSWR
is an interesting component in
react
hooks, both as a request library and as a cache for state management, and SWR is named after "stale-while-revalidate", a cache update strategy proposed in http RFC 5861:
First take the data from the cache, then go to the truth to request the corresponding data, and finally compare the cache value with the latest value, if the cache value is the same as the latest value, do not need to update, otherwise update the cache with the latest value, and update the UI presentation.
useSWR
can be used as a request library:
//fetch
import useSWR from 'swr'
import fetch from 'unfetch'
const fetcher = url => fetch(url).then(r => r.json())
function App () {
const { data, error } = useSWR('/api/data', fetcher)
// ...
}
//axios
const fetcher = url => axios.get(url).then(res => res.data)
function App () {
const { data, error } = useSWR('/api/data', fetcher)
// ...
}
//graphql
import { request } from 'graphql-request'
const fetcher = query => request('https://api.graph.cool/simple/v1/movies', query)
function App () {
const { data, error } = useSWR(
`{
Movie(title: "Inception") {
releaseDate
actors {
name
}
}
}`,
fetcher
)
// ...
}
In addition, because the same
key
always returns the same instance, only one
cache
instance is saved in
useSWR
so
useSWR
can also be used as a global state manager.
For example, you can save the user name globally:
import useSWR from 'swr';
function useUser(id: string) {
const { data, error } = useSWR(`/api/user`, () => {
return {
name: 'yuxiaoliang',
id,
};
});
return {
user: data,
isLoading: !error && !data,
isError: error,
};
}
export default useUser;
The use of specific swr is not the focus of this article, you can look at the documentation, this article uses an example to lead to an understanding of the swr principle:
const sleep = async (times: number) => {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, times);
});
};
const { data: data500 } = useSWR('/api/user', async () => {
await sleep(500);
return { a: '500 is ok' };
});
const { data: data100 } = useSWR('/api/user', async () => {
await sleep(100);
return { a: '100 is ok' };
});
What is the output of data100 and data500 in the code above?
The answer is:
Both data100 and data500 have been output with the output of 'a:'500 is ok'.
The reason is also simple: in swr default time (the default is
2000
milliseconds), for
key
of the same
useSWR
the
key
here is
‘/api/user’
is cleared with duplicate values, and only the first
key
fetcher
function is always cached for updates within
2000
milliseconds.
With this example, let's take a closer look at the source code for swr
Let's start with
useSWR
API and read the source code for swr.
First in swr it is essentially a cache update policy in memory, so in the
cache.ts
file, the cached
map
is saved.
class Cache implements CacheInterface {
constructor(initialData: any = {}) {
this.__cache = new Map(Object.entries(initialData))
this.__listeners = []
}
get(key: keyInterface): any {
const [_key] = this.serializeKey(key)
return this.__cache.get(_key)
}
set(key: keyInterface, value: any): any {
const [_key] = this.serializeKey(key)
this.__cache.set(_key, value)
this.notify()
}
keys() {
}
has(key: keyInterface) {
}
clear() {
}
delete(key: keyInterface) {
}
serializeKey(key: keyInterface): [string, any, string] {
let args = null
if (typeof key === 'function') {
try {
key = key()
} catch (err) {
// dependencies not ready
key = ''
}
}
if (Array.isArray(key)) {
// args array
args = key
key = hash(key)
} else {
// convert null to ''
key = String(key || '')
}
const errorKey = key ? 'err@' + key : ''
return [key, args, errorKey]
}
subscribe(listener: cacheListener) {
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
let isSubscribed = true
this.__listeners.push(listener)
return () => {
//unsubscribe
}
}
// Notify Cache subscribers about a change in the cache
private notify() {
}
The above is the definition of
cache
class, the essence is very simple, maintains a
map
object, indexed with
key
where
key
can be a string, function, or array,
key
key serialization method is:
serializeKey
serializeKey(key: keyInterface): [string, any, string] {
let args = null
if (typeof key === 'function') {
try {
key = key()
} catch (err) {
// dependencies not ready
key = ''
}
}
if (Array.isArray(key)) {
// args array
args = key
key = hash(key)
} else {
// convert null to ''
key = String(key || '')
}
const errorKey = key ? 'err@' + key : ''
return [key, args, errorKey]
}
From the definition of the above method, we can see that:
key
is a string, the string is the serialized
key
key
is a function, then executing the function returns a serialized
key
key
is an array, the
hash
value after the hash method (similar
hash
algorithm, the value of the array is unique after serialization) is
key
In addition, in the
cache
class, the cached object
map
which holds
key
and
value
information, is saved in the instance object
this.__cache
and the
this.__cache
object is a
map
with set get and other methods.
In swr, various events can be configured, and when an event is triggered, a corresponding requery or update function can be triggered.
Swr automatically updates the cache for these events, such as disconnecting, switching
tab
to refocus on a
tab
and so on.
The code for handling events in swr is:
const revalidate = revalidators => {
if (!isDocumentVisible() || !isOnline()) return
for (const key in revalidators) {
if (revalidators[key][0]) revalidators[key][0]()
}
}
// focus revalidate
window.addEventListener(
'visibilitychange',
() => revalidate(FOCUS_REVALIDATORS),
false
)
window.addEventListener('focus', () => revalidate(FOCUS_REVALIDATORS), false)
// reconnect revalidate
window.addEventListener(
'online',
() => revalidate(RECONNECT_REVALIDATORS),
false
)
FOCUS_REVALIDATORS
Above,
RECONNECT_REVALIDATORS
Corresponding Update Cache Function Is Saved In The RECONNECT_REVALIDATORS Event,
Triggering EventS When The Page TriggerS EventS VisibilitychangE (Show Hidden), Focus (Page Focus), And Online (Disconnected ReconNation) To Automatically Update The Cache.
useSWR
is the principal function of swr, which determines how to cache and how to update, let's first look at the references and parameters of
useSWR
Ginseng:
key
A unique value, which can be a string, function, or array that uniquely identifies
key
in the cache
fetcher
(optional) function that returns data
options
Optional for some configuration items of
useSWR
such as whether events automatically trigger cache updates, and so on.
Out:
data
The
value
value of the
key
in the cache that corresponds to the parameter
key
error
Errors that occur during the request process, etc
isValidating
Whether you are requesting or updating the cache can be used as an identity such as
isLoading
mutate(data?, shouldRevalidate?)
: Update the function to manually update the
value
value of the
key
From in and out, what we're essentially doing is
cache
instance, and the key to this
map
update is:
When you need to take values directly from the cache, when you need to re-request, update the values in the cache.
const stateRef = useRef({
data: initialData,
error: initialError,
isValidating: false
})
const CONCURRENT_PROMISES = {} //以key为键,value为新的通过fetch等函数返回的值
const CONCURRENT_PROMISES_TS = {} //以key为键,value为开始通过执行函数获取新值的时间戳
Let's look at the core function of cache updates: revalidate
// start a revalidation
const revalidate = useCallback(
async (
revalidateOpts= {}
) => {
if (!key || !fn) return false
revalidateOpts = Object.assign({ dedupe: false }, revalidateOpts)
let loading = true
let shouldDeduping =
typeof CONCURRENT_PROMISES[key] !== 'undefined' && revalidateOpts.dedupe
// start fetching
try {
dispatch({
isValidating: true
})
let newData
let startAt
if (shouldDeduping) {
startAt = CONCURRENT_PROMISES_TS[key]
newData = await CONCURRENT_PROMISES[key]
} else {
if (fnArgs !== null) {
CONCURRENT_PROMISES[key] = fn(...fnArgs)
} else {
CONCURRENT_PROMISES[key] = fn(key)
}
CONCURRENT_PROMISES_TS[key] = startAt = Date.now()
newData = await CONCURRENT_PROMISES[key]
setTimeout(() => {
delete CONCURRENT_PROMISES[key]
delete CONCURRENT_PROMISES_TS[key]
}, config.dedupingInterval)
}
const shouldIgnoreRequest =
CONCURRENT_PROMISES_TS[key] > startAt ||
(MUTATION_TS[key] &&
(startAt