Persist Apollo Cache to local storage

Hi,
I was wondering if it is possible to use apollo-cache-persist with RedwoodApolloProvider.

This would be the necessary code roughly:

import { InMemoryCache } from '@apollo/client/core';
import { persistCache, LocalStorageWrapper } from 'apollo3-cache-persist';

const cache = new InMemoryCache({...});

// await before instantiating ApolloClient, else queries might run before the cache is persisted
await persistCache({
  cache,
  storage: new LocalStorageWrapper(window.localStorage),
});

// Continue setting up Apollo as usual.

const client = new ApolloClient({
  cache,
  ...
});

Unfortunately the persistCache needs to wrap InMemoryCache before the client is created. Otherwise queries will run before cache is persisted.
Therefore useApolloClient won´t suit us.

I could pass a custom cache to RedwoodApolloClient, but too be honest I do not dare to do this, since there seems to be quite some custom logic in the normal client with fragments, suspense and stuff. (redwood/packages/web/src/apollo/index.tsx at e94dbf5ff52bc33b364e183bba47985f683c915f · redwoodjs/redwood · GitHub)

Any idea how I could accomplish this easily?

1 Like

Have you tried the method shown to customize the cache config here: GraphQL | RedwoodJS Docs ?

Ah… going to have a look at it! Hope it will work.

Unfortunately cacheConfig goes into InMemoryCache (redwood/packages/web/src/apollo/index.tsx at e26395f46e0bd131890680b7a6b6e72b6330fa97 · redwoodjs/redwood · GitHub)
And I would need the output of InMemoryCache to wrap it with persiststCache.

It is a bit a hen and egg case. Unfortunately the linked file also says InMemoryCache should be used within RedwoodApolloProvider.

Now we could consider another prop for the Provider, like beforeAssignCache: (cache: InMemoryCache): InMemoryCache => void. But this one would need to be async. Because the data from local storage is loaded async.
Alternatively we could add another RedwoodApolloPersistProvider. Would this make sense?

If we find a preferred way, I could go forward and try to implement it.

1 Like

Or perhaps make a PR to add an option to persist the cache in redwood/packages/web/src/apollo/index.tsx at e1e3f35e657743eac545e7592f1a6cb774f27b05 · redwoodjs/redwood · GitHub

1 Like

Hi @dennemark I completely forgot about this idea with v7.

Somehow the docs for the new useCache hook didn’t make it in the release and I am including them now.

I think there is a way:

First: yarn workspace web add apollo3-cache-persist

Then … somewhere :slight_smile: this is a page, but might be better in in App.tsx maybe?

import { persistCache, LocalStorageWrapper } from 'apollo3-cache-persist'

import { useCache } from '@redwoodjs/web/apollo'

const CachePage = async () => {
  const { cache } = useCache()

  await persistCache({
    cache,
    storage: new LocalStorageWrapper(window.localStorage),
  })

//.. do stuff :slight_smile: 

I then looked in Web Dev Storage settings and saw the key and my cached data.

And as I used my app, and navigated to the pages that made additional queries,I did see local storage being updated with new info.

Could you try that and let me know if it works?

If so, I’ll definitely include it in these updated caching docs,

2 Likes

i am going to try as soon as i am on v7!
thanks a lot! also did not find time yet to figure out solution…

I have tried this, but am not fully sure if this solution works. It sometimes seems like graphql requests are called, and data is available only afterwards. Although data is persisted in cache.

const SCHEMA_VERSION = '3'// has to be updated every time the schema changes
const SCHEMA_VERSION_KEY = 'apollo-schema-version'

const CacheApp = ({ children }: PropsWithChildren) => {
  const { cache } = useCache()
  const [persisted, setPersisted] = useState(false)
  useEffect(() => {
    const currentVersion = window.localStorage.getItem(SCHEMA_VERSION_KEY)
    const persistor = new CachePersistor({
      cache,
      storage: new LocalStorageWrapper(window.localStorage),
    })
    if (currentVersion === SCHEMA_VERSION) {
      persistor.restore().then(() => {
        setPersisted(true)
      })
    } else {
      persistor.purge().then(() => {
        setPersisted(true)
        window.localStorage.setItem(SCHEMA_VERSION_KEY, SCHEMA_VERSION)
      })
    }
  }, [])
  if (persisted) {
    return <>{children}</>
  } else {
    return <>Loading</>
  }
}

....
 <RedwoodApolloProvider.... >
   <CacheApp>
     ....
   </CacheApp>
 </RedwoodApolloProvider>

Apollo-cache-provider definitely caches and restores data. But the App experience feels the same. If I reload, the data is queried and only after graphql finishes, the data is properly shown.
I do have the feeling, that it is related to how cells work. I haven’t looked into the code, but if the loading cell is always shown, when apollos loading value of useQuery returns true, of course no cached data will be shown.

I was not expecting this to be added in a hook like useEffect but rather just once — maybe in a layout.

May I ask why you need to persist the cache?

Are you trying to have something that detects if the user is using your app but their cached data doesn’t match the current schema? Like their app is using schema v1 and the api is now on v2?

If so this is something the team has discussed addressing — and maybe there is another way.

Also you can clear or reset the cache on logout if you want to.

I could probably also use useLayoutEffect. It is just important, that all children are only loaded after cache is restored. The recommended way is to restore cache before assigning it to apollo, but since this would only be possible within RedwoodApolloProvider, I have done it afterwards. I guess the outcome should be quite similar.

Concerning logout and reset on schema change i just followed the FAQ readme of apollo-cache-provider.

I wanted to persist cache for large tables. Since apollo does not support @stream yet, we have to query sometimes 5mb of data and that takes a while. So i wanted to keep a version in cache. In the future this might even allow offline usage.

Have you tried setting the cache-first fetch policy in the before query of a cell?

See Queries - Apollo GraphQL Docs

By default Redwood cells uses cache and network:

1 Like

Also to properly cache anything, Apollo client requires all object to send a typename and an id. So the table and also each row etc would need these so it can make a cache id.

We have so new cache docs for canary v7 docs: Adds GraphQL Caching docs for Client and Response caching by dthyresson · Pull Request #10054 · redwoodjs/redwood · GitHub and this highlights an Apollo client browser extension that lets you explore the cache.

See Developer tools - Apollo GraphQL Docs

I’ve added an issue to add a way to persist the cache more easily in the framework.

Please do provide some more details so can consider this when we implement.

Thanks!

Also, have you considered response caching to reduce load on database to fetch the 5MB data?

See new docs: redwood/docs/docs/graphql/caching.md at 8b870b289557d19d1685865ce1766f5f7572dcce · redwoodjs/redwood · GitHub

Thanks a lot! I am currently investigating other issues, so I would really try the cache-first approach, maybe even cache-only, if it exists. Since this would be great for debugging. Or did you already try it?

Normally I would have thought cache-and-network should be fine.

Response caching is something i should investigate…yes.

I’d think, so too – but it will still pull down the 5MB and try to match up – so, do try out the Developer tools - Apollo GraphQL Docs

so you can be sure the cache the cache you expect.

BTW - could the table be paginated to reduce the row could and 5MB size?

I wouldn’t mind if data is pulled from network, as long as the cached data is shown already in the beginning. Since users won’t have to wait for data.

Pagination is an option, but only if i move all the filtering sorting etc. to the backend, and I think this might not be fun. Though it could be useful. I am much more interested though in streaming for apollog-client :slight_smile:
It is a bit of a notion-like block structure.

It seems to work. If I load the page first with cache-and-network, switch to cache-only, the data is loaded properly. If I set cache-only and do a reload with clearing cache, it fails, since nothing is in cache of course. So I assume it all works.

Thanks a lot for your help!

1 Like