SWR is a hook component, which can be used as a request library and state management library. This article mainly introduces how to use SWR in the project, and analyzes the principle of SWR. Read the SWR source code from the fundamentals

  • What is the SWR
  • SWR source code

This article is from my blog: github.com/fortheallli…

What is SWR

UseSWR is a function of the stale-while-revalidate protocol, which is used in HTTP RFC 5861 as a cache update policy. It is a function of the stale-while-revalidate protocol, which is used in HTTP RFC 5861.

Finally, the cached value is compared with the latest value. If the cached value is the same as the latest value, no update is required. Otherwise, the latest value is used to update the cache and update the UI display effect.

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
  )
  // ...
}
Copy the code

In addition, because the same key always returns the same instance, only one cache instance is stored in useSWR, so useSWR can also be used as a global state management machine. 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;Copy the code

The specific usage of SWR is not the focus of this article. For details, please refer to the documentation. This article uses an example to introduce the understanding of 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' };
});
Copy the code

Data100 and data500 are output in the above code.

The answer is:

Data100 and data500 {a:’500 is OK ‘}

The reason is simple: in the default SWR time (2000 ms by default), for the same useSWR key, where the key is’/API /user ‘, the duplicate value is cleared, and only the fetcher function of the first key within 2000 ms is used for cache updates.

With this example, let’s dig into the SWR source code

SWR source code

Let’s start with the useSWR API and read the SWR source code. First, SWR is essentially an in-memory cache update strategy, so cache maps are stored in cache.ts.

(1) cache. Ts cache

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() { }Copy the code

The cache class maintains a map object indexed by a key. The key can be a string, function, or array. The serializeKey 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]
  }
  
  
Copy the code

From the definition of the above method we can see:

  • If the key passed in is a string, the string is the serialized key
  • If the key passed in is a function, the function is executed and the result returned is the serialized key
  • If the key passed in is an array, the value serialized by the hash method is the key.

In addition, in the cache class, the cache object map, which holds the key and value information, is stored in the instance object this.__cache.

(2) Event processing

In SWR, you can configure events that, when triggered, trigger the corresponding rerequest or update function. SWR automatically updates the cache for events such as disconnection and reconnection, switching a TAB to refocus a TAB, and so on by default.

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 )Copy the code

FOCUS_REVALIDATORS, RECONNECT_REVALIDATORS events save the corresponding update cache functions. When a page triggers events visibilityChange (show hide), Focus (page focus) and Online (disconnection and reconnection), events will be triggered to automatically update the cache.

(3) useSWR cache update body function

UseSWR is the body function of SWR, which determines how to cache and update. Let’s first look at useSWR’s input and parameter.

The arguments:

  • Key: A unique value, which can be a string, function, or array, that uniquely identifies the key in the cache
  • Fetcher: (Optional) a function that returns data
  • Options: (Optional) Some configuration items for useSWR, such as whether events automatically trigger cache updates, etc.

Reference:

  • Data: value of the key corresponding to the input key in the cache
  • Error: Indicates an error generated during a request
  • IsValidating: If the cache is being requested or updated, you can use this as an identifier, such as isLoading.
  • mutate(data? , shouldRevalidate?) : Update function to manually update the value of the corresponding key

From input to output, what we’re essentially doing is controlling the cache instance. The key to updating this map is:

When do you need to take the value directly from the cache, and when do you need to re-request, update the value in the cache.

const stateRef = useRef({ data: initialData, error: initialError, isValidating: Const CONCURRENT_PROMISES_TS = {} const CONCURRENT_PROMISES = {} const CONCURRENT_PROMISES_TS = {} const CONCURRENT_PROMISES_TS = {} Value is the timestamp to start retrieving the new value by executing the functionCopy the code

Let’s look at the core function for 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 <= MUTATION_TS[key] || startAt <= MUTATION_END_TS[key] || MUTATION_END_TS[key] === 0)) if (shouldIgnoreRequest)  { dispatch({ isValidating: false }) return false } cache.set(key, newData) cache.set(keyErr, undefined) // new state for the reducer const newState: actionType<Data, Error> = { isValidating: false } if (typeof stateRef.current.error ! == 'undefined') { // we don't have an error newState.error = undefined } if (! config.compare(stateRef.current.data, newData)) { // deep compare to avoid extra re-render // data changed newState.data = newData } // merge the new state dispatch(newState) if (! shouldDeduping) { // also update other hooks broadcastState(key, newData, undefined) } } catch (err) { // catch err } loading = false return true }, [key] )Copy the code

The above code has been simplified, so dispatch is a function that updates the return value of useSWR:

const stateDependencies = useRef({
    data: false,
    error: false,
    isValidating: false
})
const stateRef = useRef({
    data: initialData,
    error: initialError,
    isValidating: false
})
let dispatch = useCallback(payload => {
let shouldUpdateState = false
for (let k in payload) {
  stateRef.current[k] = payload[k]
  if (stateDependencies.current[k]) {
    shouldUpdateState = true
  }
}
if (shouldUpdateState || config.suspense) {
  if (unmountedRef.current) return
  rerender({})
 }
}, [])
Copy the code

In the above dispath function, we update stateRef as needed. The value returned by stateRef is the final value returned by useSWR. Rerender is a hook that has been updated by react-hooks:

const rerender = useState(null)[1]
Copy the code

Each time rerender({}) is executed, an overall update of the component within the hook function is triggered. Second, let’s be clear again:

// const CONCURRENT_PROMISES = {} // const CONCURRENT_PROMISES = {} // const CONCURRENT_PROMISES = {} // Const CONCURRENT_PROMISES = {} // const CONCURRENT_PROMISES = {} // const CONCURRENT_PROMISES = {} // const CONCURRENT_PROMISES = {} // const CONCURRENT_PROMISES = {} // const CONCURRENT_PROMISES = {} // const CONCURRENT_PROMISES = {} // Value is the timestamp to start retrieving the new value by executing the functionCopy the code

Moving on to the core of the revalidate update function:

let shouldDeduping = typeof CONCURRENT_PROMISES[key] ! == 'undefined' && revalidateOpts.dedupe 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) }Copy the code

The default value of config.dedupingInterval is 2000 milliseconds, that is, in 2000 milliseconds, the same key will be dedupingInterval, that is, if in 2000 milliseconds, If multiple update functions are initiated for the same key at the same time, the result of the first update will prevail. CONCURRENT_PROMISES_TS (); CONCURRENT_PROMISES (); CONCURRENT_PROMISES ();

A set of values of the deleted key and value within a certain period of time. The key is the unique key in the useSWR, that is, the key of the cache instance Map, and the value is the latest updated value in the cache.

(4) How to update in useSWR

CONCURRENT_PROMISES_TS (); CONCURRENT_PROMISES_TS; CONCURRENT_PROMISES_TS (); How do we update the return value of the useSWR instance when the value of the CONCURRENT_PROMISES_TS key changes?

Let’s look at the code:

// Save the object const CACHE_REVALIDATORS = {} // specific update function const onUpdate: updaterInterface<Data, Error> = ( shouldRevalidate = true, updatedData, updatedError, dedupe = true ) => { // update hook state const newState: actionType<Data, Error> = {} let needUpdate = false if ( typeof updatedData ! == 'undefined' && ! config.compare(stateRef.current.data, updatedData) ) { newState.data = updatedData needUpdate = true } if (stateRef.current.error ! Error = updatedError needUpdate = true} // Update current stateRef if (needUpdate) { Dispatch (newState)} if (shouldRevalidate) {return revalidate()} return false (revalidators, callback) => { if (! callback) return if (! revalidators[key]) { revalidators[key] = [callback] } else { revalidators[key].push(callback) } } Const broadcastState: (broadcast_revalidators, onUpdate) broadcastStateInterface = (key, data, error) => { const updaters = CACHE_REVALIDATORS[key] if (key && updaters) { for (let i = 0; i < updaters.length; ++i) { updaters[i](false, data, error) } } }Copy the code

The broadcastState method fires every time a cache key is updated, while CACHE_REVALIDATORS holds all key-related update functions.

Why is the value of CACHE_REVALIDATORS[key] an array?

Because useSWR keys can have multiple update functions for the same key, CACHE_REVALIDATORS[key] is an array.

For example, it is permissible to use two keys with the same name in the same component, but their update functions are different:

 const { data: data500 } = useSWR('/api/user', async () => {
    await sleep(500);
    return { message: '500 is ok' };
 });
 const { data: data100 } = useSWR('/api/user', async () => {
    await sleep(100);
    return { message: '100 is ok' };
 });
 
Copy the code

(5) Mutate actively triggers the update function

Knowing the updates in useSWR, the remaining mutate is pretty simple:

const mutate: mutateInterface = async ()=>{ let data, error if (_data && typeof _data === 'function') { // `_data` is a function, call it passing current cache value try { data = await _data(cache.get(key)) } catch (err) { error = err } } else if (_data && typeof _data.then === 'function') { // `_data` is a promise try { data = await _data } catch (err) { error = err } } else { data = _data } .... const updaters = CACHE_REVALIDATORS[key] if (updaters) { const promises = [] for (let i = 0; i < updaters.length; ++i) { promises.push(updaters[i](!! shouldRevalidate, data, error, i > 0)) } // return new updated value return Promise.all(promises).then(() => { if (error) throw error return cache.get(key) }) } }Copy the code

Simply take the value and call each update function in the const updaters = CACHE_REVALIDATORS[key] array to update the corresponding useSWR value. The value of data can be fetched directly from the cache or passed in manually (similar to optimistic updates).