Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Custom Cache Support #212

Closed
sergiodxa opened this issue Dec 23, 2019 · 10 comments · Fixed by #231
Closed

[RFC] Custom Cache Support #212

sergiodxa opened this issue Dec 23, 2019 · 10 comments · Fixed by #231
Labels
discussion Discussion around current or proposed behavior RFC

Comments

@sergiodxa
Copy link
Contributor

sergiodxa commented Dec 23, 2019

Goal

Enable SWR users to control the cache.

This is based on the Cache collection PR and other issues such as #4, #16, #158, #161.

Background

Right now there is no way to control the cache used by SWR, this means:

  • We can't use another cache implementation (Support multiple caching mechanisms #16), maybe a project needs time based LRU, another could use size based LRU, another could use a completely different cache mechanism which could range from cache everything to cache nothing and request again every time, even use a SSR-friendly cache for Next.js usage or change where we keep the cached data (e.g. use localStorage to keep an offline cache)
  • Data fetched on tests it's shared between tests (Have a way to clear cache #161) and could not be easily cleared.
  • There is not way to clear the cache or delete cache items individually (Remove old cached items #4), this could be useful if we deleted a cached item and want to clear the cache, mutating to undefined will not work

Proposal

I propose an API to support changing the cache through the SWRConfig component.

const customConfig = {
  cache: CustomCacheImplementation
};

<SWRConfig value={customConfig}>
  <App />
</SWRConfig>;

With this, App and any component inside it calling useSWR will use this cache implementation.

The custom cache should only be customized with SWRConfig and not in a per useSWR instance, if you want to use a custom cache in a certain instance the parent should wrap the component in SWRConfig, this way the components are not aware of the cache mechanism used.

Using multiple caches

This could also allow using multiple cache implementations in different parts of the app, you could render another SWRConfig deep inside the component tree of App to change the cache in that branch of the tree.

When this could be useful? Maybe we now certain parts should be cached offline (e.g with localStorage or IndexedDB) and others should be cached in-memory, we will be able to customize it.

Default Cache

By default SWR will come with a cache implementation, e.g. it could use the one implemented in #92, this will allow most projects to don't care about the cache implementation.

Testing

If we are testing components calling useSWR we could wrap the render of the component in SWRConfig with their own cache instance.

// app.test.js
import React from "react";
import { SWRConfig } from "swr";
import { render, screen } from "@testing-library/react";
import { CacheInMemory } from "in-memory-cache-implementation";
import App from "./app";

test("should render with data", async () => {
  jest.once(JSON.stringify({ user: { username: "evilrabbit" } }));
  render(
    <SWRConfig value={{ cache: new CacheInMemory() }}>
      <App />
    </SWRConfig>
  );
  expect(await screen.findByText("evilrabbit")).toBeInTheDocument();
});

Now every test will use their on in-memory cache and will not share previously fetched data between them.

Using a new cache instance per test will simplify running tests in parallel without causing any issue because we clear the shared cache before another test finished first.

Controlling the Cached Data

Since we could create the cache instance in their own module we should be able to import it anywhere and control it.

// cache.js
class CustomCache {
  // implementation here
}

export default new CustomCache();
// random-component.js
import cache from "./cache";

function RandomComponent() {
  // code here
  React.useEffect(() => {
    // clear cache key once the component unmounted
    return () => cache.del("cache-key");
  }, []);
  // code here
}

Cache Interface

The cache should be an object, similar to a JS Map, including events to let instances of SWR subscribe to it. The exposed interface could be something like this:

type Key = string;
type Event = "set" | "delete";

interface Cache {
  set<Data>(key: Key, value: Data): Data; // save data to a key, create or update
  get<Data>(key: Key): Data | null; // return the data stored in the key or null
  delete(key: Key): void; // delete a single key
  has(key: Key): boolean; // check if a key is in cache
  clear(): void; // delete all key
  on(event: Event, callback: (key: Key) => void);
  off(event: Event, callback: (key: Key) => void);
}

Any custom cache should implement that interface, internally they could use any caching mechanism, e.g. a localStorage based cache which will store everything there forever.

// CacheEmitter could provide the on/off/emit methods
// it could be EventEmitter or something custom provided by SWR itself
class LocalStorageCache extends CacheEmitter implements Cache {
  set<Data>(key: Key, value: Data): Data {
    let stringifiedValue = value;
    if (typeof value !== "string") {
      stringifiedValue = JSON.stringify(value);
    }
    localStorage.set(key, value);
    this.emit("set", key);
  }

  get<Data>(key: Key): Data | null; {
    const stored = localStorage.getItem(key);
    if (!stored) return null;
    return JSON.parse(stored);
  }

  delete(key: Key): void {
    localStorage.removeItem(key);
    this.emit("delete", key);
  }

  has(key: Key): boolean {
    const value = this.get(key);
    return value !== null;
  }

  clear(): void {
    const keys: Key[] = Object.keys(localStorage);
    localStorage.clear();
    keys.forEach(key => this.emit("delete", key));
  }
}
@shuding
Copy link
Member

shuding commented Dec 26, 2019

Do you think it's possible to support for async cache (maybe multilayer cache as well)?

I've been thinking for a while and have some ideas to extend those with the same API.
First we can make all async cache 2 layers, a (sync) memcache + (async) cache.

So for async (multlayer) cache, get(key) does 2 things:

  1. always returns the latest value v0 (from the memcache layer, edge cache) synchronously
  2. (async) get the value v1 from the source cache (e.g.: IndexedDB), and compare it with v0,
    if they're different:
    1. update the value in memcache with v1
    2. mutate(key, v1) to notify all the hooks with the latest value

@shuding shuding added the discussion Discussion around current or proposed behavior label Dec 26, 2019
@sergiodxa
Copy link
Contributor Author

I like your idea for async cache, I think having a sync way to read cache it's always better to avoid running an effect to get the currently cached data, and since we know the cache key it could be possible to run mutate and fill the cache correctly.

compare it with v0

I'm not sure about that, since the result it's probably going to be an object or array in most APIs the check will always return false since the object/array will be a new one.

@shuding
Copy link
Member

shuding commented Dec 26, 2019

@sergiodxa I think we have to rely on deep comparison.

@cryptiklemur
Copy link
Contributor

Add the option to specify your own comparator?

@sergiodxa
Copy link
Contributor Author

@aequasi if you implement your own cache you could use your own comparator, this way a cache could be faster because it doesn't use deep comparison or could be more correct because it does it, so you could pick between two similar options based on that, depending on your requirements.

@sergiodxa sergiodxa mentioned this issue Jan 14, 2020
@crobinson42
Copy link

What about not offering "cache" support and instead exposing a few simple lifecycle methods and events for the following:

  • hydrate
  • add/remove/update

With these exposed then there will be the possibility for everyone to create their own implementation or cache how they wish.

@nandorojo
Copy link

@shuding Are there docs anywhere for using the cache? Thanks!

@sergiodxa
Copy link
Contributor Author

@nandorojo there is no doc yet, but you can read the code https://github.com/zeit/swr/blob/master/src/cache.ts, it’s short

@nandorojo
Copy link

@sergiodxa Looks like the cache isn't async, is there a way to use it with something like React Native's AsyncStorage? Or did you end up deciding against that?

@isonlaxman
Copy link

Any update on this? I'm looking to use React Native's AsyncStorage to persist the cache as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion Discussion around current or proposed behavior RFC
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants