apollo-cache-persist icon indicating copy to clipboard operation
apollo-cache-persist copied to clipboard

apollo-cache-persist missing example with support for reactive variables

Open thijssmudde opened this issue 5 years ago • 30 comments

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?

thijssmudde avatar Oct 02 '20 00:10 thijssmudde

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

ivnnv avatar Oct 02 '20 16:10 ivnnv

Thats a separate issue, but you can fix it with:

persistCache({
  cache,
  storage: window.localStorage as PersistentStorage<PersistedData<NormalizedCacheObject>>,
})

thijssmudde avatar Oct 02 '20 17:10 thijssmudde

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.

ivnnv avatar Oct 02 '20 17:10 ivnnv

Could be. Good point! But there's no support for reactive variables at all in this library.

thijssmudde avatar Oct 02 '20 17:10 thijssmudde

@ivnnv please check sample application. It works with typescript.

wtrocki avatar Oct 02 '20 19:10 wtrocki

@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 avatar Oct 02 '20 19:10 wtrocki

@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?

ivnnv avatar Oct 02 '20 22:10 ivnnv

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])

thijssmudde avatar Oct 02 '20 23:10 thijssmudde

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

wtrocki avatar Oct 03 '20 04:10 wtrocki

Added minimal example of using a a reactive var to store the selected values for the currencies list!

ivnnv avatar Oct 03 '20 15:10 ivnnv

Superb work :)

wtrocki avatar Oct 03 '20 17:10 wtrocki

Superb work :)

Am I missing something? The example does not appear to work. How does it persist?

rebz avatar Oct 09 '20 21:10 rebz

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 avatar Oct 09 '20 21:10 pillowsoft

@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,

wtrocki avatar Oct 09 '20 21:10 wtrocki

@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

ivnnv avatar Oct 10 '20 06:10 ivnnv

Relevant recent PR from @PedroBern: https://github.com/apollographql/apollo-client/pull/7148

benjamn avatar Oct 15 '20 18:10 benjamn

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 avatar Oct 16 '20 10:10 PedroBern

@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 avatar Oct 16 '20 11:10 ivnnv

@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! ;)

PedroBern avatar Oct 17 '20 14:10 PedroBern

Is this available? I'm migrating everything from Redux so would like to use reactive variables, but everything is lost on page refresh...

j-lee8 avatar Aug 19 '21 18:08 j-lee8

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 avatar Aug 20 '21 11:08 wodCZ

@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!

j-lee8 avatar Aug 20 '21 12:08 j-lee8

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.

wtrocki avatar Aug 20 '21 12:08 wtrocki

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 avatar Sep 03 '21 13:09 timothyarmes

@timothyarmes Why use isString instead of typeof value === 'string'?

raarts avatar Sep 04 '21 15:09 raarts

@timothyarmes Why use isString instead of typeof 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)

timothyarmes avatar Sep 05 '21 16:09 timothyarmes

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?

rrubo avatar Sep 22 '21 18:09 rrubo

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.

timothyarmes avatar Sep 23 '21 07:09 timothyarmes

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 image

gendalf-thug avatar Aug 26 '22 08:08 gendalf-thug

'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

DanRioDev avatar Sep 15 '22 04:09 DanRioDev