Enable SWR users to control the cache.
This is based on the Cache collection PR and other issues such as #4, #16, #158, #161.
Right now there is no way to control the cache used by SWR, this means:
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.
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.
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.
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.
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
}
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));
}
}
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:
v0 (from the memcache layer, edge cache) synchronously v1 from the source cache (e.g.: IndexedDB), and compare it with v0,v1mutate(key, v1) to notify all the hooks with the latest valueI 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.
@sergiodxa I think we have to rely on deep comparison.
Add the option to specify your own comparator?
@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.
What about not offering "cache" support and instead exposing a few simple lifecycle methods and events for the following:
With these exposed then there will be the possibility for everyone to create their own implementation or cache how they wish.
@shuding Are there docs anywhere for using the cache? Thanks!
@nandorojo there is no doc yet, but you can read the code https://github.com/zeit/swr/blob/master/src/cache.ts, it鈥檚 short
@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?
Most helpful comment
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:v0(from the memcache layer, edge cache) synchronouslyv1from the source cache (e.g.: IndexedDB), and compare it withv0,if they're different:
v1mutate(key, v1)to notify all the hooks with the latest value