Repository: zeit/swr (ts)

Issue:
Expose cache to users
I saw from #4 that there's a plan to expose SWR's (currently internal) cache to users at some point - is there some ETA on that?

My main use case right now would be to improve SSR with Next.js - [with the presented example code](https://github.com/zeit/swr#ssr-with-nextjs) (similar to code I have in my own Next.js app), `getInitialProps` will refetch the data *every time* before switching to the page, even if data already exists for that page in the SWR cache. Thus with exposed cache you could do something along the lines of this instead:

```typescript
import useSWR, { cacheGet, cacheSet } from "swr";

const path = "/api/data";

App.getInitialProps = async () => {
  let data;  
  if (typeof window === "undefined") {
    // server-side getInitialProps
    data = await fetcher(path); // always fetch on server
  } else {
    // client-side getInitialProps
    data = cacheGet(path); // check cache for data
    if (!data) {
      data = await fetcher(path); // only fetch if data not available in cache
      cacheSet(path, data); // update cache since we got the data for it
     }
  }
  return { data };
}

function App (props) {
  const initialData = props.data;
  const { data } = useSWR(path, fetcher, { initialData });

  return <div>{ data }</div>;
}
```

This way, you get those nice server side rendered pages while also making in-app navigation snappier for pages you've already visited.
Have a way to clear cache
I'm writing tests for a component using `useSWR`, also with `{ suspense: true }`, and if I render a component reading the same key (e.g. the same component with different keys) I'm getting the old cache in my second test.

Not sure if this will be addressed with #158, but having a way to tell SWR to clear the cache of a certain key would be awesome for this use case.

```js
import { clearCache } from "swr";

clearCache(); // clear all cache
clearCache("/api/data"); // clear only a certain cache key
```

☝️ something like that.

What I have tried:

- `mutate(key, null, false)`
- `mutate(key, {}, false)`

Both doesn't work right since useSWR will still read that `null` or `{}` from the cache instead of triggering an immediate fetch and suspending the component.

I will try to create a CodeSandbox to replicate this soon.
[RFC] Custom Cache Support
## Goal

Enable SWR users to control the cache.

This is based on the [Cache collection PR](https://github.com/zeit/swr/pull/92) 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 (#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 (#161) and could not be easily cleared.
- There is not way to clear the cache or delete cache items individually (#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.

```js
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.

```js
// 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.

```js
// cache.js
class CustomCache {
  // implementation here
}

export default new CustomCache();
```

```js
// 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:

```ts
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.

```ts
// 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));
  }
}
```

put initialData in cache
### description 

when populating a `useSWR` hook with initialData, that data is per-hook and not stored in the cache to be available to other hook invocations, resulting in duplicate fetches for `useSWR` invocations that don't have the SSR initialData passed to them.

for example:

0. in a next.js project a page's `getInitialProps` function is generating `initialData` as in the example.
1. the page component does `useSWR(someKey, { initialData })` and correctly SSR renders the initialData.
2. a child component also invokes `useSWR(someKey)`. the child component's `useSWR` sees an empty cache and executes fetcher, resulting in an unnecessary fetch.

### workarounds

1. prop-drill or context-pass initialData to all the components that need it
2. only use `useSWR` in the parent and prop-drill / context-pass the return value of `useSWR` to components that need it

### expectation

when initialData is provided, it will be stored in the cache under the provided key. when other `useSWR` hooks are invoked, they'll see the cached value and avoid an extra call to fetcher


Pull request description:
Expose Cache
This attempts to partially resolve #212 and resolves #161 and resolves #237 and resolves #158

## Implementation Notes

I updated this to only expose the global cache, I kept the cache interface, adding a way to tell the `set`, `delete` and `clear` methods if you want to notify components reading from the cache, this way `mutate` can call `cache.set` and `cache.set` can also call `mutate` as a notification method without running on a infinite loop (mutate calls `cache.set` with this flag as `false`).

I did this to come back to custom cache once I found a way to let `mutate` know which cache to call without manually passing it.

~I created a CacheInterface and make the global config create a default instance of the cache, also exported both the interface and the Cache class.~

~I also updated `useSWR` to read from the cache instance received through the context object.~

~I was not able, yet, to find a way to make `mutate` and `trigger` read from the correct cache instance, because they are global my only idea so far was to pass the cache instance as a third argument (probably optional), but that could make it hard to test components using a custom cache for tests and a default cache for the mutate call.~

## Missing things
~- `useSWRPages` is still using global cache (I haven't tried yet to use it here but I believe is the same as `mutate` and `trigger`)~
~- `mutate` is still using global cache~
~- `trigger` is still using global cache~

Any feedback is welcomed!

Patch:
diff --git a/examples/axios-typescript/libs/useRequest.ts b/examples/axios-typescript/libs/useRequest.ts
index a747c72..b06837f 100644
--- a/examples/axios-typescript/libs/useRequest.ts
+++ b/examples/axios-typescript/libs/useRequest.ts
@@ -27,16 +27,25 @@ export default function useRequest<Data = unknown, Error = unknown>(
   const { data: response, error, isValidating, revalidate } = useSWR<
     AxiosResponse<Data>,
     AxiosError<Error>
-  >(request && JSON.stringify(request), () => axios(request || {}), {
-    ...config,
-    initialData: initialData && {
-      status: 200,
-      statusText: 'InitialData',
-      config: request,
-      headers: {},
-      data: initialData
+  >(
+    request && JSON.stringify(request),
+    /**
+     * NOTE: Typescript thinks `request` can be `null` here, but the fetcher
+     * function is actually only called by `useSWR` when it isn't.
+     */
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    () => axios(request!),
+    {
+      ...config,
+      initialData: initialData && {
+        status: 200,
+        statusText: 'InitialData',
+        config: request,
+        headers: {},
+        data: initialData
+      }
     }
-  })
+  )
 
   return {
     data: response && response.data,
diff --git a/package.json b/package.json
index 53b2ed7..a1cee00 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "swr",
-  "version": "0.1.17",
+  "version": "0.1.18",
   "description": "React Hooks library for remote data fetching",
   "main": "./dist/index.js",
   "module": "./esm/index.js",
diff --git a/src/cache.ts b/src/cache.ts
new file mode 100644
index 0000000..77efb4d
--- /dev/null
+++ b/src/cache.ts
@@ -0,0 +1,99 @@
+import { CacheInterface, keyInterface, cacheListener } from './types'
+import { mutate } from './use-swr'
+import hash from './libs/hash'
+
+export default class Cache implements CacheInterface {
+  private __cache: Map<string, any>
+  private __listeners: cacheListener[]
+
+  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, shouldNotify = true): any {
+    const [_key] = this.serializeKey(key)
+    this.__cache.set(_key, value)
+    if (shouldNotify) mutate(key, value, false)
+    this.notify()
+  }
+
+  keys() {
+    return Array.from(this.__cache.keys())
+  }
+
+  has(key: keyInterface) {
+    const [_key] = this.serializeKey(key)
+    return this.__cache.has(_key)
+  }
+
+  clear(shouldNotify = true) {
+    if (shouldNotify) this.__cache.forEach(key => mutate(key, null, false))
+    this.__cache.clear()
+    this.notify()
+  }
+
+  delete(key: keyInterface, shouldNotify = true) {
+    const [_key] = this.serializeKey(key)
+    if (shouldNotify) mutate(key, null, false)
+    this.__cache.delete(_key)
+    this.notify()
+  }
+
+  // TODO: introduce namespace for the cache
+  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 () => {
+      if (!isSubscribed) return
+      isSubscribed = false
+      const index = this.__listeners.indexOf(listener)
+      if (index > -1) {
+        this.__listeners[index] = this.__listeners[this.__listeners.length - 1]
+        this.__listeners.length--
+      }
+    }
+  }
+
+  // Notify Cache subscribers about a change in the cache
+  private notify() {
+    for (let listener of this.__listeners) {
+      listener()
+    }
+  }
+}
diff --git a/src/config.ts b/src/config.ts
index 6f74219..5024cde 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -6,21 +6,10 @@ import {
   RevalidateOptionInterface,
   revalidateType
 } from './types'
+import Cache from './cache'
 
-// Cache
-const __cache = new Map()
-
-function cacheGet(key: string): any {
-  return __cache.get(key)
-}
-
-function cacheSet(key: string, value: any) {
-  return __cache.set(key, value)
-}
-
-function cacheClear() {
-  __cache.clear()
-}
+// cache
+const cache = new Cache()
 
 // state managers
 const CONCURRENT_PROMISES = {}
@@ -107,8 +96,6 @@ export {
   FOCUS_REVALIDATORS,
   CACHE_REVALIDATORS,
   MUTATION_TS,
-  cacheGet,
-  cacheSet,
-  cacheClear
+  cache
 }
 export default defaultConfig
diff --git a/src/index.ts b/src/index.ts
index 2603021..e3820cb 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,11 +1,13 @@
 export * from './use-swr'
 import { default as useSWR } from './use-swr'
 export { useSWRPages } from './use-swr-pages'
+export { cache } from './config'
 export {
   ConfigInterface,
   revalidateType,
   RevalidateOptionInterface,
   keyInterface,
-  responseInterface
+  responseInterface,
+  CacheInterface
 } from './types'
 export default useSWR
diff --git a/src/types.ts b/src/types.ts
index 41d1832..6921637 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -115,3 +115,16 @@ export type actionType<Data, Error> = {
   error?: Error
   isValidating?: boolean
 }
+
+export interface CacheInterface {
+  get(key: keyInterface): any
+  set(key: keyInterface, value: any, shouldNotify?: boolean): any
+  keys(): string[]
+  has(key: keyInterface): boolean
+  delete(key: keyInterface, shouldNotify?: boolean): void
+  clear(shouldNotify?: boolean): void
+  serializeKey(key: keyInterface): [string, any, string]
+  subscribe(listener: cacheListener): () => void
+}
+
+export type cacheListener = () => void
diff --git a/src/use-swr-pages.tsx b/src/use-swr-pages.tsx
index 5e1b06a..27659b0 100644
--- a/src/use-swr-pages.tsx
+++ b/src/use-swr-pages.tsx
@@ -1,6 +1,6 @@
 import React, { useCallback, useMemo, useState, useRef } from 'react'
 
-import { cacheGet, cacheSet } from './config'
+import { cache } from './config'
 import {
   pagesResponseInterface,
   responseInterface,
@@ -100,10 +100,10 @@ export function useSWRPages<OffsetType = any, Data = any, Error = any>(
   const pageOffsetKey = `_swr_page_offset_` + pageKey
 
   const [pageCount, setPageCount] = useState<number>(
-    cacheGet(pageCountKey) || 1
+    cache.get(pageCountKey) || 1
   )
   const [pageOffsets, setPageOffsets] = useState<OffsetType[]>(
-    cacheGet(pageOffsetKey) || [null]
+    cache.get(pageOffsetKey) || [null]
   )
   const [pageSWRs, setPageSWRs] = useState<responseInterface<Data, Error>[]>([])
 
@@ -134,7 +134,7 @@ export function useSWRPages<OffsetType = any, Data = any, Error = any>(
   const loadMore = useCallback(() => {
     if (isLoadingMore || isReachingEnd) return
     setPageCount(c => {
-      cacheSet(pageCountKey, c + 1)
+      cache.set(pageCountKey, c + 1)
       return c + 1
     })
   }, [isLoadingMore || isReachingEnd])
@@ -167,7 +167,7 @@ export function useSWRPages<OffsetType = any, Data = any, Error = any>(
             setPageOffsets(arr => {
               const _arr = [...arr]
               _arr[id + 1] = newPageOffset
-              cacheSet(pageOffsetKey, _arr)
+              cache.set(pageOffsetKey, _arr)
               return _arr
             })
           }
diff --git a/src/use-swr.ts b/src/use-swr.ts
index 566d4fb..a6eec4c 100644
--- a/src/use-swr.ts
+++ b/src/use-swr.ts
@@ -9,15 +9,13 @@ import {
 } from 'react'
 
 import defaultConfig, {
-  cacheGet,
-  cacheSet,
   CACHE_REVALIDATORS,
   CONCURRENT_PROMISES,
   CONCURRENT_PROMISES_TS,
   FOCUS_REVALIDATORS,
-  MUTATION_TS
+  MUTATION_TS,
+  cache
 } from './config'
-import hash from './libs/hash'
 import isDocumentVisible from './libs/is-document-visible'
 import isOnline from './libs/is-online'
 import throttle from './libs/throttle'
@@ -42,41 +40,18 @@ const IS_SERVER = typeof window === 'undefined'
 // useLayoutEffect in the browser.
 const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect
 
-// TODO: introduce namepsace for the cache
-const getErrorKey = key => (key ? 'err@' + key : '')
-const getKeyArgs = key => {
-  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 || '')
-  }
-
-  return [key, args]
-}
-
 const NO_DEDUPE = false
 
 const trigger: triggerInterface = (_key, shouldRevalidate = true) => {
-  const [key] = getKeyArgs(_key)
+  // we are ignoring the second argument which correspond to the arguments
+  // the fetcher will receive when key is an array
+  const [key, , keyErr] = cache.serializeKey(_key)
   if (!key) return
 
   const updaters = CACHE_REVALIDATORS[key]
   if (key && updaters) {
-    const currentData = cacheGet(key)
-    const currentError = cacheGet(getErrorKey(key))
+    const currentData = cache.get(key)
+    const currentError = cache.get(keyErr)
     for (let i = 0; i < updaters.length; ++i) {
       updaters[i](shouldRevalidate, currentData, currentError, NO_DEDUPE)
     }
@@ -97,7 +72,7 @@ const mutate: mutateInterface = async (
   _data,
   shouldRevalidate = true
 ) => {
-  const [key] = getKeyArgs(_key)
+  const [key] = cache.serializeKey(_key)
   if (!key) return
 
   // if there is no new data, call revalidate against the key
@@ -111,7 +86,7 @@ const mutate: mutateInterface = async (
   if (_data && typeof _data === 'function') {
     // `_data` is a function, call it passing current cache value
     try {
-      data = await _data(cacheGet(key))
+      data = await _data(cache.get(key))
     } catch (err) {
       error = err
     }
@@ -127,8 +102,8 @@ const mutate: mutateInterface = async (
   }
 
   if (typeof data !== 'undefined') {
-    // update cached data
-    cacheSet(key, data)
+    // update cached data, avoid notifying from the cache
+    cache.set(key, data, false)
   }
 
   // update existing SWR Hooks' state
@@ -179,10 +154,8 @@ function useSWR<Data = any, Error = any>(
   // we assume `key` as the identifier of the request
   // `key` can change but `fn` shouldn't
   // (because `revalidate` only depends on `key`)
-  const [key, fnArgs] = getKeyArgs(_key)
-
   // `keyErr` is the cache key for error objects
-  const keyErr = getErrorKey(key)
+  const [key, fnArgs, keyErr] = cache.serializeKey(_key)
 
   config = Object.assign(
     {},
@@ -196,8 +169,8 @@ function useSWR<Data = any, Error = any>(
     fn = config.fetcher
   }
 
-  const initialData = cacheGet(key) || config.initialData
-  const initialError = cacheGet(keyErr)
+  const initialData = cache.get(key) || config.initialData
+  const initialError = cache.get(keyErr)
 
   // if a state is accessed (data, error or isValidating),
   // we add the state to dependencies so if the state is
@@ -282,7 +255,7 @@ function useSWR<Data = any, Error = any>(
 
           // if no cache being rendered currently (it shows a blank page),
           // we trigger the loading slow event.
-          if (config.loadingTimeout && !cacheGet(key)) {
+          if (config.loadingTimeout && !cache.get(key)) {
             setTimeout(() => {
               if (loading) config.onLoadingSlow(key, config)
             }, config.loadingTimeout)
@@ -316,8 +289,8 @@ function useSWR<Data = any, Error = any>(
           return false
         }
 
-        cacheSet(key, newData)
-        cacheSet(keyErr, undefined)
+        cache.set(key, newData, false)
+        cache.set(keyErr, undefined, false)
         keyRef.current = key
 
         // new state for the reducer
@@ -348,7 +321,7 @@ function useSWR<Data = any, Error = any>(
         delete CONCURRENT_PROMISES[key]
         delete CONCURRENT_PROMISES_TS[key]
 
-        cacheSet(keyErr, err)
+        cache.set(keyErr, err, false)
         keyRef.current = key
 
         // get a new error
@@ -399,7 +372,7 @@ function useSWR<Data = any, Error = any>(
     // and trigger a revalidation
 
     const currentHookData = stateRef.current.data
-    const latestKeyedData = cacheGet(key) || config.initialData
+    const latestKeyedData = cache.get(key) || config.initialData
 
     // update the state if the key changed or cache updated
     if (
@@ -570,8 +543,8 @@ function useSWR<Data = any, Error = any>(
     // (it should be suspended)
 
     // try to get data and error from cache
-    let latestData = cacheGet(key)
-    let latestError = cacheGet(keyErr)
+    let latestData = cache.get(key)
+    let latestError = cache.get(keyErr)
 
     if (
       typeof latestData === 'undefined' &&


Test patch:
diff --git a/test/use-swr.test.tsx b/test/use-swr.test.tsx
index e8c28f9..dae9801 100644
--- a/test/use-swr.test.tsx
+++ b/test/use-swr.test.tsx
@@ -7,8 +7,8 @@ import {
 } from '@testing-library/react'
 import React, { ReactNode, Suspense, useEffect, useState } from 'react'
 
-import useSWR, { mutate, SWRConfig, trigger } from '../src'
-import { cacheSet } from '../src/config'
+import useSWR, { mutate, SWRConfig, trigger, cache } from '../src'
+import Cache from '../src/cache'
 
 class ErrorBoundary extends React.Component<{ fallback: ReactNode }> {
   state = { hasError: false }
@@ -956,7 +956,7 @@ describe('useSWR - local mutation', () => {
 
   it('should call function as data passing current cached value', async () => {
     // prefill cache with data
-    cacheSet('dynamic-15', 'cached data')
+    cache.set('dynamic-15', 'cached data')
     const callback = jest.fn()
     await mutate('dynamic-15', callback)
     expect(callback).toHaveBeenCalledWith('cached data')
@@ -1251,3 +1251,72 @@ describe('useSWR - suspense', () => {
     expect(renderedResults).toEqual(['suspense-7', 'suspense-8'])
   })
 })
+
+describe('useSWR - cache', () => {
+  it('should react to direct cache updates', async () => {
+    cache.set('cache-1', 'custom cache message')
+
+    function Page() {
+      const { data } = useSWR('cache-1', () => 'random message', {
+        suspense: true
+      })
+      return <div>{data}</div>
+    }
+
+    // render using custom cache
+    const { queryByText, findByText } = render(
+      <React.Suspense fallback={null}>
+        <Page />
+      </React.Suspense>
+    )
+
+    // content should come from custom cache
+    expect(queryByText('custom cache message')).toMatchInlineSnapshot(`
+      <div>
+        custom cache message
+      </div>
+    `)
+
+    // content should be updated with fetcher results
+    expect(await findByText('random message')).toMatchInlineSnapshot(`
+      <div>
+        random message
+      </div>
+    `)
+
+    act(() => cache.set('cache-1', 'a different message'))
+
+    // content should be updated from new cache value
+    expect(await findByText('a different message')).toMatchInlineSnapshot(`
+      <div>
+        a different message
+      </div>
+    `)
+
+    act(() => cache.delete('cache-1'))
+
+    // content should go back to be the fetched value
+    expect(await findByText('random message')).toMatchInlineSnapshot(`
+      <div>
+        random message
+      </div>
+    `)
+  })
+
+  it('should notify subscribers when a cache item changed', async () => {
+    // create new cache instance to don't get affected by other tests
+    // updating the normal cache instance
+    const tmpCache = new Cache()
+
+    const listener = jest.fn()
+    const unsubscribe = tmpCache.subscribe(listener)
+    tmpCache.set('cache-2', 'random message')
+
+    expect(listener).toHaveBeenCalled()
+
+    unsubscribe()
+    tmpCache.set('cache-2', 'a different message')
+
+    expect(listener).toHaveBeenCalledTimes(1)
+  })
+})