SWR Custom Cache

Brief

SWR is a react hook for data fetching. Its strategy is to first return the data from cache (stale), then send the fetch request (revalidate), and finally come with the up-to-date data.

There's not much document mentioned the cache API officially listed in v0 'cause it was not well designed and usages are not clear. In the past few months we've been trying to ship custom cache feature for SWR to solve a few problems brought up by community.

Few strong desires from community:

  • Access data from cache
  • Monitor cache changes
  • Port cache to various kinds of storage
  • Partial mutation with cache
  • Clean up cache state in testing
  • etc...

Legacy Cache

The legacy cache, is always a hidden gem that we don't want user to find it so there's nothing mentioned about it in the documentation. How's it look like? It's just a Map instance, and what's more, it's a global instance. It also provide method like subscribe to monitor the cache changes. Those features are not really make us feel confident to let user to use since it's kind of getting far from the origin principle of react, though it solves users' problems.

SWR provides contextual APIs like SWRConfig to let users set configs effecting all hooks under it. Global vs Context sounds like a huge conflict, especially the global cache is also exposed.

Let's take a look at the approach to resolve problems early mentioned in this post.

Monitor cache changes and port to other storage

The subscribe mode feels heavy here since you get all changes.

// access cache states
const state = Array.from(cache.keys()).map(key => ([key, cache.get(value)]))

// port cache state to local storage
localStorage.setItem(JSON.stringify(state))

Sounds no problem so far. But imagine this, you want to persist different kinds of data into different storage. For example putting your preferred website settings like themes into localStorage, and saving user profile in memory cache.

// ❌ global cache cannot deal with these at the same time

// persist into browser local storage for longer time 
const { data: { theme } } = useSWR(settingKey, fetchSetting)

// just cache in memory cache during whole session
const { data: user } = useSWR(userId, fetchUser)

How can we approach this with only one global cache? Might be impossible.

Other JS Runtime - React Native

SWR is mainly designed for web platform, but seems people are starting to use it in their React Native (RN) projects. SWR did provided some amazing features like "revalidate on focus" and "revalidate on reconnect". However with the change of the platform, the runtime API to bind events are becoming different. RN doesn't have any events like 'focus' or 'online' due to complete different experience on mobile and porting device platform.

The global events binding setup in SWR is not working for this case. I started to think whether it's possible to bring this into the next version of cache API.

Cache API in v1

The custom cache API is now introduced into SWR beta versions, which is intended to solve the problems above.

Let's take a peek how it looks like

Hook useSWRConfig() + Option provider

import { useSWRConfig } from 'swr'

function App() {
  return (
    <SWRConfig value={{ provider: () => new Map() }}>
      <Page/>
    </SWRConfig>
  )
}

function Page() {
  // `cache` and `mutate` are bound to the scope of SWR context
  const { cache, mutate } = useSWRConfig()
}

The provider is a data layer we use to store cache state. It should provide the basic operations of cache abstraction: set(key, value), get(key) and delete(key).

Usage

To use the provider, you can directly pass to a hook or put it into context provider SWRConfig, same as other options. And then your entire application can be split into different cache boundaries.

// custom cache provider of local storage
<SWRConfig value={{ provider: localStorageCache }}>
  <Theme />
</SWRConfig>

// custom cache provider of local storage
<SWRConfig value={{ provider: indexDBProvider}}>
  <Dashboard />
</SWRConfig>

To solve the custom observation of focus and online events on other JS runtime, there're 2 experimental options initFocus and initReconnect for user to setup their own events management. In a short word, they receive a revalidate callback for manually triggering revalidation, like code snippet bellow

<SWRConfig 
  value={{
    initFocus(revalidate) {
      // detect when is on focus and call `revalidate()` to update SWR state
    } 
  }}
>
  <Page>
</SWRConfig>

Checkout SWR - React Native for more details.

Hidden story - There was a version that using createCache(provider, options) API to generate cache and pass it to SWRConfig. But with few iterations we found that it's better to embrace hook based API. Finally we choose to use provider for creating custom cache on SWRConfig level and consuming by useSWRConfig in hook level.

Now it make much more sense. Come and try it out! Please reach out to SWR issues for feedback.