Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
thijssmudde opened this issue Oct 2, 2020 · 30 comments

Comments

@thijssmudde
Copy link

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?

@ivnnv
Copy link
Contributor

ivnnv commented Oct 2, 2020

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

@thijssmudde
Copy link
Author

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

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

@ivnnv
Copy link
Contributor

ivnnv commented Oct 2, 2020

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.

@thijssmudde
Copy link
Author

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

@wtrocki
Copy link
Collaborator

wtrocki commented Oct 2, 2020

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

@wtrocki
Copy link
Collaborator

wtrocki commented Oct 2, 2020

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

@ivnnv
Copy link
Contributor

ivnnv commented Oct 2, 2020

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

@thijssmudde
Copy link
Author

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

@wtrocki
Copy link
Collaborator

wtrocki commented Oct 3, 2020

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 wtrocki changed the title How to use apollo-cache-persist with reactive variable? apollo-cache-persist missing example with support for reactive variables Oct 3, 2020
@ivnnv
Copy link
Contributor

ivnnv commented Oct 3, 2020

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

@wtrocki
Copy link
Collaborator

wtrocki commented Oct 3, 2020

Superb work :)

@rebz
Copy link

rebz commented Oct 9, 2020

Superb work :)

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

@pillowsoft
Copy link

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?

@wtrocki
Copy link
Collaborator

wtrocki commented Oct 9, 2020

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

@ivnnv
Copy link
Contributor

ivnnv commented Oct 10, 2020

@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

@benjamn
Copy link
Member

benjamn commented Oct 15, 2020

Relevant recent PR from @PedroBern: apollographql/apollo-client#7148

@PedroBern
Copy link

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

@ivnnv
Copy link
Contributor

ivnnv commented Oct 16, 2020

@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

@PedroBern
Copy link

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

@j-lee8
Copy link

j-lee8 commented Aug 19, 2021

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

@wodCZ
Copy link
Collaborator

wodCZ commented Aug 20, 2021

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

@j-lee8
Copy link

j-lee8 commented Aug 20, 2021

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

@wtrocki
Copy link
Collaborator

wtrocki commented Aug 20, 2021

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.

@timothyarmes
Copy link

timothyarmes commented Sep 3, 2021

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.

@raarts
Copy link

raarts commented Sep 4, 2021

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

@timothyarmes
Copy link

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

@rrubo
Copy link

rrubo commented Sep 22, 2021

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?

@timothyarmes
Copy link

timothyarmes commented Sep 23, 2021

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.

@gendalf-thug
Copy link

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

@oVerde
Copy link

oVerde commented Sep 15, 2022

'Cmon someone makes this a PR! 🏆

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests