apollo-cache-persist
apollo-cache-persist copied to clipboard
apollo-cache-persist missing example with support for reactive variables
I want to automatically persist reactive variable value so the data will still be there after refreshing the page.
Given a reactive variable
import { ReactiveVar } from '@apollo/client'
import Disclaimer from 'dskcore/@interfaces/Disclaimer'
export default (disclaimerVar: ReactiveVar<Disclaimer>) => {
return (value: Disclaimer) => {
disclaimerVar(value)
}
}
export const disclaimerVar: ReactiveVar<Disclaimer> = makeVar<Disclaimer>(
initialDisclaimer
)
export const cache: InMemoryCache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
disclaimer: {
read () {
return disclaimerVar()
}
}
}
}
}
})
How can I make the reactive variable persist?
Im also trying to accomplis this, but Typescript maybe is giving us a clue because:
TS2322: Type 'InMemoryCache' is not assignable to type 'ApolloCache<unknown>'.
when trying to:
const cache = new InMemoryCache(cacheConfig);
await persistCache({
cache,
value: window.localStorage,
});
on client initialization
PS: Im using @apollo/client @ 3.2.0
Thats a separate issue, but you can fix it with:
persistCache({
cache,
storage: window.localStorage as PersistentStorage<PersistedData<NormalizedCacheObject>>,
})
Thats a separate issue, but you can fix it with:
persistCache({ cache, storage: window.localStorage as PersistentStorage<PersistedData<NormalizedCacheObject>>, })
Well, thats not really a fix because you are basically @ts-ignore-error doing that aliasing type of thing.
If Typescript complains is because the definitions are not exactly equal (and they should be), so probably it is related with the fact that it is not working.
Could be. Good point! But there's no support for reactive variables at all in this library.
@ivnnv please check sample application. It works with typescript.
@fullhdpixel Reactive variables will not work with persistence. That is true. PR's welcome although this is not problem with library. It is more how apollo client works.
@wtrocki thanks for the heads up, I assumed (and I think @fullhdpixel too) this could be used to make reactiveVars to persist, because it would be a game changer to make apollo the definitive state manager solution for every case. So could you advice how we could make this work? Im interested on trying investigating for creating a PR to make it happen.
A temporary solution could be to create a wrapper in between the setter getter of the reactive vars and localStorage?
Here is a temporary fix outside of apollo-cache-persist
Lets say you have a reactiveVar mutation like so, you can do the localStorage.setItem in this mutation operation.
import { ReactiveVar } from '@apollo/client'
import FilterStageObject from 'dskcore/@interfaces/FilterStageObject'
export default (filterVar: ReactiveVar<FilterStageObject>) => {
return (value: FilterStageObject) => {
localStorage.setItem('filter', JSON.stringify(value))
filterVar(value)
}
}
In your component you retrieve the information from the localStorage in a React.useEffect
const [filter, setFilterState] = React.useState<FilterStageObject>(initialFilterState)
const { data: dataFilter } = useQuery(GET_FILTER)
React.useEffect(() => {
// every time dataFilter changes we get it from the localStorage
const newFilterState: FilterStageObject = JSON.parse(localStorage.getItem('filter')) || initialFilterState
setFilterState(newFilterState)
}, [dataFilter])
First of all we need to determine how reactive vars are implemented. Are they stored in the InMemoryCache. Best start for me will be to add reactive var to example react web in this repo.
Once that is done we can see if there is API that we can hook into. If there is that will be very quick fix. There is also performance consideration for reactive vars persistence etc.but I will skip that for the moment.
CC @benjamn
Added minimal example of using a a reactive var to store the selected values for the currencies list!
Superb work :)
Superb work :)
Am I missing something? The example does not appear to work. How does it persist?
I ran the example app and while it uses reactiveVars, it does not seem to persist. I am thus assuming that this is still an issue and the example app was just to demo this issue? Any news on fixing this? I see the apollo dev tools also can't "see" the reactive vars, so I imagine there needs to be an API to expose them to other packages?
@pillowsoft I tried quickly get into where vars are created - they are separate from cache it seems but could not pinpoint it. For me it looks like reactive vars persistence will require figuring out apollo-client internals,
@rebz @pillowsoft the example indeed "doesn't work" in terms of persistence. The example got updated for the apollo team to have a minimum viable demo app at hand to find out a way to make reactive vars persistent
Relevant recent PR from @PedroBern: https://github.com/apollographql/apollo-client/pull/7148
Reactive variables persistence isn't an Apollo/apollo-cache-persist issue, it's just a regular js issue. Here is the most barebones example of how to do it, very similar to my PR mentioned by @benjamn, that tries to simplify this process.
// global scope
export const myVar = makeVar<T>(value)
// inside my root component (did mount hook for example)
// render a loading indicator while it's not ready
const previousValue = await restoreAsync(key)
myVar(previousValue)
setReady(true)
// every time I want to update the value
const update = (value:T) => {
myVar(value)
saveToTheStorageAsync(key, value)
}
@PedroBern thats fantastic news! Thank you very much for the apollo-client PR, ill be one of the first testers once that is merged for sure
@ivnnv nice to hear that! You don't need to wait, just copy the source code from the PR and import the reactive variables from there, it's just one file! ;)
Is this available? I'm migrating everything from Redux so would like to use reactive variables, but everything is lost on page refresh...
There is currently no support for persisting/restoring reactive variables in apollo-cache-persist library. When I was looking into it, I didn't find a way to attach to apollo client/cache in a way that would magically save/restore all reactive variables. (that of course doesn't mean there isn't a way, I just didn't find it).
@wodCZ I appreciate you taking the time to check. Yeah, this is mildly frustrating. I'll remain with Apollo for the caching and the persistence will remain with Redux (massively overkill for what I'm building but it's already setup and can be swapped out in the future). Can use reactive variables for other things which is good.
I will keep an eye on this for the future. It would certainly become a game changer when ready!
There is immerse complexity and performance toll of building this support generic way. Some elements of our code can be reused but this would be most likely separate codebase/setup.
I think it would be easy to hack this in your app with separate storage as for support in cache persist it will require some investigation etc.
Hi,
Inspired by other answers here's my solution to the problem, implemented in Typescript, and using localStorage. I'm sure someone here can adapt it to use async-type storage if they need it. Note that this is a 'clean' implementation that doesn't involve monkey-patching makeVar:
import { makeVar, ReactiveVar } from '@apollo/client';
import { isString } from 'lodash';
const getCleanValueForStorage = (value: unknown) => {
return isString(value) ? value : JSON.stringify(value);
};
const makeVarPersisted = <T>(initialValue: T, storageName: string): ReactiveVar<T> => {
let value = initialValue;
// Try to fetch the value from local storage
const previousValue = localStorage.getItem(storageName);
if (previousValue !== null) {
try {
const parsed = JSON.parse(previousValue);
value = parsed;
} catch {
// It wasn't JSON, assume a valid value
value = (previousValue as unknown) as T;
}
}
// Create a reactive var with stored/initial value
const rv = makeVar<T>(value);
const onNextChange = (newValue: T | undefined) => {
try {
// Try to add the value to local storage
if (newValue === undefined) {
localStorage.removeItem(storageName);
} else {
localStorage.setItem(storageName, getCleanValueForStorage(newValue));
}
} catch {
// ignore
}
// Re-register for the next change
rv.onNextChange(onNextChange);
};
// Register for the first change
rv.onNextChange(onNextChange);
return rv;
};
export default makeVarPersisted;
Using it is as simple as:
export const loginToken = makeVarPersisted<string | undefined>(undefined, 'myVariable');
The value will try to initialise from localStorage, and fall back on the default. Updating will automatically update local storage too.
@timothyarmes Why use isString instead of typeof value === 'string'?
@timothyarmes Why use
isStringinstead oftypeof value === 'string'?
No reason really. I happen to be using lodash, and the other code example that I based this version on did the same thing (but they didn't use the onNextChange mechanism)
For me, the whole point of using the Reactive Variables was avoiding storing any auth-related info (i.e. tokens) in the local storage.
Using it is as simple as:
export const loginToken = makeVarPersisted<string | undefined>(undefined, 'loginToken');
Is this solution secure in terms of storing tokens in the local storage?
For me, the whole point of using the Reactive Variables was avoiding storing any auth-related info (i.e. tokens) in the local storage.
Sure, it's best not to store auth tokens, and to store refresh tokens as HTTP only cookies. However that's really nothing to do with this post which is really about persisting reactive variables for whatever reason you might have. I'll change the local storage key in my example to avoir any confusion.
I have improved the retention of the reactive variable in the example above
import { makeVar, ReactiveVar } from "@apollo/client";
import { isString } from "lodash";
import AsyncStorage from "@react-native-async-storage/async-storage";
const getCleanValueForStorage = (value: unknown) => {
return isString(value) ? value : JSON.stringify(value);
};
export const getVarPersisted = async <T>(
rv: ReactiveVar<T>,
storageName: string
) => {
let value;
// Try to fetch the value from local storage
const previousValue = await AsyncStorage.getItem(storageName);
if (previousValue !== null) {
try {
const parsed = await JSON.parse(previousValue);
value = parsed;
} catch {
value = previousValue as unknown as T;
}
}
value && rv(value);
const onNextChange = (newValue: T | undefined) => {
try {
if (newValue === undefined) {
AsyncStorage.removeItem(storageName);
} else {
AsyncStorage.setItem(storageName, getCleanValueForStorage(newValue));
}
} catch (err) {
console.log("🚀 - err", err);
}
rv.onNextChange(onNextChange);
};
rv.onNextChange(onNextChange);
};
export const countVar = makeVar(0);
Use case

'Cmon someone makes this a PR! :trophy:
Can't count how many hours I lost trying to figure out a lib named cache-persist wasn't persisting the basic apollo local state example