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

Is it possible to immediately load persisted data? #346

Closed
aidan-keay opened this issue Mar 30, 2021 · 19 comments
Closed

Is it possible to immediately load persisted data? #346

aidan-keay opened this issue Mar 30, 2021 · 19 comments
Labels
middleware/persist This issue is about the persist middleware

Comments

@aidan-keay
Copy link

aidan-keay commented Mar 30, 2021

I'm using the persist middleware and loading state from local storage. As far as I am aware, there is a brief period where the store state is the default state and then the store is rehydrated with the persisted state. Is there any way to load this immediately if the storage access method is synchronous? It works if I load from localStorage manually at the top of the file and set this as the store state although I would rather use the middleware.

@AnatoleLucet
Copy link
Collaborator

I thought about conditionally making the middleware's hydration sync or async. But I don't know if people would find this confusing or not.
Any thoughts?

@sandren
Copy link

sandren commented Mar 30, 2021

I thought about conditionally making the middleware's hydration sync or async. But I don't know if people would find this confusing or not.
Any thoughts?

@AnatoleLucet We're addressing the same concern here with Jotai as well.

Right now the Jotai atomWithStorage utility is set up to update the state when it is first mounted to accommodate SSR in Next.js and Gatsby, but perhaps the persistence implementation in both libraries could be made to support both sync and async with an extra parameter similar to how both will have a getStorage parameter with a default function of () => window.localStorage.

Maybe we could both add a fourth parameter ssrMode, which defaults to true or false?

I'm curious what @dai-shi thinks as well.

@dai-shi
Copy link
Member

dai-shi commented Mar 30, 2021

I thought about conditionally making the middleware's hydration sync or async.

I think it's possible if we check the return value of getItem is instanceof Promise and otherwise process synchronously. That's how jotai works internally.

@aidan-keay
Copy link
Author

aidan-keay commented Mar 30, 2021

I think it's possible if we check the return value of getItem is instanceof Promise and otherwise process synchronously. That's how jotai works internally.

Will this also work with async deserialization and migration functions?

@AnatoleLucet
Copy link
Collaborator

@aidan-keay yes, we would just need to check each of these three.

@ChrisKG32
Copy link

I have noticed this problem too, as the SSR will render a page with the default persisted state, and not trigger a re-render with the actual persisted state.

@axelboc
Copy link

axelboc commented May 3, 2021

I bumped into this after I started injecting a zustand store through context as shown in #182 (comment).

Somewhere in the children of the store's provider, I have a useEffect that sets something in the store on mount. Turns out this useEffect is called before the store is rehydrated from localStorage.


UPDATE

I managed to work around the issue with a local boolean state and the onRehydrateStorage option:

function initialiseStore(onRehydrated: () => void) {
  return create<MyConfig>(
    persist(
      (set, get) => ({ ... }),
      {
        name: 'my-config',
        onRehydrateStorage: () => onRehydrated,
      }
    )
  );
}

export function MyConfigProvider(props: { children: ReactNode }) {
  const { children } = props;

  // https://github.com/pmndrs/zustand/issues/346
  const [reydrated, setRehydrated] = useState(false);

  // https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
  const [useStore] = useState(() => {
    return initialiseStore(() => setRehydrated(true));
  });

  return reydrated ? <Provider value={useStore}>{children}</Provider> : null;
}

@dai-shi dai-shi added the middleware/persist This issue is about the persist middleware label May 18, 2021
@tkvw
Copy link

tkvw commented May 24, 2021

Thanks everyone!
I created a small utility function createPersistentContext:

Usage:

const {Provider,useStore,useStoreApi} = createPersistentContext(stateCreator,{
    name: "storekey",
    getStorage: () => localStorage
});

The provider allows a override of the persistent options and a fallback component whilst rehydrating.

Implementation:

import React, {PropsWithChildren, useMemo, useState} from "react";

import create, {State} from "zustand";
import {StateCreator} from "zustand/vanilla";
import createContext from "zustand/context";
import {persist} from "zustand/middleware";


export function createPersistentContext<S extends State>(stateCreator: StateCreator<S>, options: PersistOptions<S>){
    const {Provider,useStore,useStoreApi} = createContext<S>();
    function PersistentProvider(props: PropsWithChildren<{
        fallback?: React.ReactNode;
        persist?: PersistOptions<S>;
    }>){
        const {children,persist: persistOptions,fallback = null} = props;
        const [rehydrated, setRehydrated] = useState(false);
        const useStore = useMemo(() => {
            const allOptions = {
                ...options,
                ...persistOptions
            };
            return create(persist(stateCreator,{
                ...allOptions,
                onRehydrateStorage: (state) => {
                    const next = allOptions.onRehydrateStorage? allOptions.onRehydrateStorage(state): undefined;
                    return (success,error) => {
                        setRehydrated(true);
                        if(next){
                            next(success,error);
                        }
                    }
                }
            }));
        },[persistOptions]);

        if(!rehydrated){
            return <>{fallback}</>;
        }
        return <Provider initialStore={useStore}>
            {children}
        </Provider>
    }
    return {
        Provider: PersistentProvider,
        useStore,
        useStoreApi
    }
}


declare type StateStorage = {
    getItem: (name: string) => string | null | Promise<string | null>;
    setItem: (name: string, value: string) => void | Promise<void>;
};
declare type StorageValue<S> = {
    state: S;
    version: number;
};

declare type PersistOptions<S> = {
    /** Name of the storage (must be unique) */
    name: string;
    /**
     * A function returning a storage.
     * The storage must fit `window.localStorage`'s api (or an async version of it).
     * For example the storage could be `AsyncStorage` from React Native.
     *
     * @default () => localStorage
     */
    getStorage?: () => StateStorage;
    /**
     * Use a custom serializer.
     * The returned string will be stored in the storage.
     *
     * @default JSON.stringify
     */
    serialize?: (state: StorageValue<S>) => string | Promise<string>;
    /**
     * Use a custom deserializer.
     *
     * @param str The storage's current value.
     * @default JSON.parse
     */
    deserialize?: (str: string) => StorageValue<S> | Promise<StorageValue<S>>;
    /**
     * Prevent some items from being stored.
     */
    blacklist?: (keyof S)[];
    /**
     * Only store the listed properties.
     */
    whitelist?: (keyof S)[];
    /**
     * A function returning another (optional) function.
     * The main function will be called before the state rehydration.
     * The returned function will be called after the state rehydration or when an error occurred.
     */
    onRehydrateStorage?: (state: S) => ((state?: S, error?: Error) => void) | void;
    /**
     * If the stored state's version mismatch the one specified here, the storage will not be used.
     * This is useful when adding a breaking change to your store.
     */
    version?: number;
    /**
     * A function to perform persisted state migration.
     * This function will be called when persisted state versions mismatch with the one specified here.
     */
    migrate?: (persistedState: any, version: number) => S | Promise<S>;
};

@AnatoleLucet
Copy link
Collaborator

Should be fixed in 3.5.2 by #403.
Let us know if there's still any trouble

@chhourbro
Copy link

Hi there, we are currently facing this issue with 3.5.7

@AnatoleLucet
Copy link
Collaborator

Hey @chhourbro, it seems to work as expected on 3.5.7 with localStorage (see https://stackblitz.com/edit/react-quueqh?file=src%2FApp.js).
What storage are you using? This issue was dedicated to sync storages.

@jankrah12
Copy link

I am using React Native's AsyncStorage, and facing this issue.

@AnatoleLucet
Copy link
Collaborator

AnatoleLucet commented Sep 9, 2021

@jankrah12 AsyncStorage is an asynchronous storage, which means we need to "wait" until it returns the data and cannot / do not want to block the render within the persist middleware.
This is not the case for synchronous storages like localStorage or sessionStorage.

If you want to do some kind of gate to prevent your app from rendering if your zustand store has not yet been hydrated with AsyncStorage's data, you can do something like the following:

const useStore = create(
  persist(
    (set, get) => ({
      // ...
      _hasHydrated: false
    }),
    {
      name: 'store',
      onRehydrateStorage: () => () => {
        useStore.setState({ _hasHydrated: true })
      }
    }
  )
);

export default function App() {
  const hasHydrated = useStore(state => state._hasHydrated);

  if (!hasHydrated) {
    return <p>Loading...</p>
  }

  return (
    // ...
  );
}

Edit: since zustand v3.6.3, you can now know if the store has been hydrated using useStore.persist.hasHydrated()

@jbrodriguez
Copy link

this didn't work for me

      onRehydrateStorage: () => (state, error) => {
        console.log('onRehydrateStorage', state, error)
        useStore.setState({ _hasHydrated: true })
      }
 LOG  onRehydrateStorage {"hydrated": false} undefined
 LOG  onRehydrateStorage undefined [TypeError: undefined is not an object (evaluating 'useStore.setState')]

had to go for

      onRehydrateStorage: () => (state) => {
        if (state) {
          state.hydrated = true
        }
      },

@jbrodriguez
Copy link

jbrodriguez commented Nov 14, 2021

actually, i've now completely removed the rehydration guard. this is because i'm using react-native-mmkv as storage engine, which is a synchronous storage.

The question is then, do we need to guard for hydration only on asynchronous storage engines ?

@AnatoleLucet
Copy link
Collaborator

You shouldn't need to check for hydration if you're using a sync storage. See this part of the doc for more details.

FYI, this is probably why you had an error when trying to use useStore.setState in onRehydrateStorage. Since the hydration is sync, create() has not returned anything (yet) when onRehydrateStorage is called.

@larsqa
Copy link

larsqa commented Sep 1, 2022

You shouldn't need to check for hydration if you're using a sync storage

FYI: This is not true, if you're using Next.js with Static Site Generation (getStaticPaths) and the persist middleware, then the server side rendered page will differ from the client side page due to the zustand store maybe receiving updated values from the client side store (this is called hydration).

#324 Explains this more in detail

@ghost
Copy link

ghost commented Jun 30, 2023

this didn't work for me

      onRehydrateStorage: () => (state, error) => {
        console.log('onRehydrateStorage', state, error)
        useStore.setState({ _hasHydrated: true })
      }
 LOG  onRehydrateStorage {"hydrated": false} undefined
 LOG  onRehydrateStorage undefined [TypeError: undefined is not an object (evaluating 'useStore.setState')]

had to go for

      onRehydrateStorage: () => (state) => {
        if (state) {
          state.hydrated = true
        }
      },

This is not correct. We have to pass the setHydrated property. Like this:

onRehydrateStorage:
 ({ setHydrated }) =>
     state => {
     setHydrated(true);
}

@VSMent
Copy link

VSMent commented Jul 1, 2023

this didn't work for me

      onRehydrateStorage: () => (state, error) => {
        console.log('onRehydrateStorage', state, error)
        useStore.setState({ _hasHydrated: true })
      }
 LOG  onRehydrateStorage {"hydrated": false} undefined
 LOG  onRehydrateStorage undefined [TypeError: undefined is not an object (evaluating 'useStore.setState')]

had to go for

      onRehydrateStorage: () => (state) => {
        if (state) {
          state.hydrated = true
        }
      },

This is not correct. We have to pass the setHydrated property. Like this:

onRehydrateStorage:
 ({ setHydrated }) =>
     state => {
     setHydrated(true);
}

It will work with produce / immer middleware

onRehydrateStorage: () =>
  produce((state) => {
    if (state) {
      state.hydrated = true
    }
  }),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
middleware/persist This issue is about the persist middleware
Projects
None yet
Development

No branches or pull requests