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

Wait for Nextjs rehydration before zustand store rehydration #938

Closed
MoustaphaDev opened this issue Apr 26, 2022 · 40 comments
Closed

Wait for Nextjs rehydration before zustand store rehydration #938

MoustaphaDev opened this issue Apr 26, 2022 · 40 comments
Labels
middleware/persist This issue is about the persist middleware

Comments

@MoustaphaDev
Copy link

Hello, I'm using the persist middleware on my store with Nextjs (SSG) and I got several warnings in dev mode all pointing:
Error: Hydration failed because the initial UI does not match what was rendered on the server.
It doesn't break the app however.

Looks like the zustand store rehydrates before Nextjs has finished its rehydration process.
Is there a way trigger the rehydration of the zustand store after Nextjs' rehydration process or are there better ways of handling this?

@MoustaphaDev MoustaphaDev changed the title Wait for rehydration before persisting store in Nextjs Wait for Nextjs rehydration before zustand store rehydration Apr 26, 2022
@dai-shi
Copy link
Member

dai-shi commented Apr 26, 2022

A confirmation: Is this persist only issue? Or, can the error be reproducible without persist?

@MoustaphaDev
Copy link
Author

Thanks for the fast reply. It is in fact persist only issue.

@dai-shi dai-shi added the middleware/persist This issue is about the persist middleware label Apr 26, 2022
@dai-shi
Copy link
Member

dai-shi commented Apr 26, 2022

@AnatoleLucet can have a look please? (I'm still not sure if this is something that can be solved only with persist.)

@MoustaphaDev Can the issue be reproducible with zustand v3.7.x?

@MoustaphaDev
Copy link
Author

Yes it can, just tried to downgrade to v3.7.x but same error.

@AnatoleLucet
Copy link
Collaborator

I'm unable to reproduce. @MoustaphaDev could you send a repro in a codesandbox?

@MoustaphaDev
Copy link
Author

Sure, here's the sandbox @AnatoleLucet.

@AnatoleLucet
Copy link
Collaborator

@MoustaphaDev Thanks.

I think this is a dup of #324

@MoustaphaDev
Copy link
Author

Alright, your useHydrated hook trick solved it, thank you!

@isomorpheric
Copy link

isomorpheric commented May 17, 2022

I'm having the same problem, can only reproduce with persist - however, I'm using Typescript. Will try useHydrated and post follow-up.

Follow-Up: Using the useHasHydrated hook works, but checking for hydration everywhere isn't very DRY. There has to be a simpler way to achieve this globally. 🤔

@AnatoleLucet
Copy link
Collaborator

@ecam900 another option is to make your storage engine async (see this comment) which might better fit your use case.

@pixelass
Copy link

pixelass commented May 30, 2022

None of the options above or from the linked issues worked for us. The only way for us to remove the warning without checking if the component was mounted (e.g. useHasHydrated via a custom useEffect), was to trick Zustand into skipping the initial render-cycle before getting the store from window.localStorage.

@AnatoleLucet If you are sure that your solution works, it would be nice if you could provide an MVP Sandbox.

I will provide a few Sandboxes with all solutions we've tested tomorrow, to allow better reproduction and show that my "workaround" works (while not optimal).

This is the solution that works as a workaround for us:

const useStore = create(
  persist(
    (set) => ({
      counter: 0,
      setCounter(counter) {
        set({ counter });
      },
    }),
    {
      name: "next-zustand",
      getStorage: () => ({
        setItem: (...args) => window.localStorage.setItem(...args),
        removeItem: (...args) => window.localStorage.removeItem(...args),
        getItem: async (...args) =>
          new Promise((resolve) => {
            if (typeof window === "undefined") {
              resolve(null);
            } else {
              setTimeout(() => {
                resolve(window.localStorage.getItem(...args));
              }, 0);
            }
          }),
      }),
    }
  )
);

The trick was to add a setTimeout inside getItem. A simple promise (or async function) still wouldn't fix the issue.

Looks ugly, feels ugly… let's face it: "It's ugly"

getItem: async (key) =>
  new Promise((resolve) => {
    setTimeout(() => {
       if (typeof window === "undefined") {
        resolve(null);
      } else {
        setTimeout(() => {
          resolve(window.localStorage.getItem(...args));
        }, 0);
     }
  }),
}),

Side-node: This issue started with React v18 since they seem to handle or warn about hydration different. This might be part of Next.js v12 or React.js v18, We are currently unsure about this.

The only "other" option that worked for us was to not render until the component was mounted on the client but that means either of the following caveats

@AlwanN01
Copy link

AlwanN01 commented Jul 4, 2022

@pixelass if your app using authentication, you can send an cookies/local storage from persist middleware to server / next page with gerServersideProps.

/**
 * @callback getServersideProps
 * @param {import('next').GetServerSidePropsContext} context
 */

/**
 * @param {getServersideProps} getServersideProps
 */
const withStore = (getServersideProps = /*default*/  () => ({props:{}}) ) =>
        /*return function*/ async context=>{
          const {req} = context
          const hasToken = req.cookies.token ? true : false
          if (!hasToken) return {redirect: {destination: '/login', permanent: false} }
          //get state from local storage / cookies
          context.state = state =>req.cookies?.[state] ? JSON.parse(req.cookies[state]).state : null
          return getServersideProps(context)
          }

export const getServerSideProps = withStore(context => {
   return {
   props:{
            myStore: context.state('myStorePersistName')
            }
         }
      }
    )
    
import {useMemo} from 'react'
import useStore from '../myStore'
export default function MyPage({myStore}) {
         useMemo(() => myStore && useStore.setState(myStore),[])
         const store = useStore(state => state.data)
         
         return <div>{store}</div>
     }

@KGDavidson
Copy link

KGDavidson commented Dec 2, 2022

I've been using a custom hook to wrap my persisted store
(ignore the types if you don't need them)

type GetFunctionKeys<T> = {
  [K in keyof T]: T[K] extends ((...args: any[]) => void) ? K : never;
}[keyof T];

type OmittedFunctionKeys<T> = Omit<T, GetFunctionKeys<T>>;

type StoreState = {
  fishes: number,
  addAFish: () => void,
};

const initialStates = {
  fishes: 0,
};

const usePersistedStore = create<StoreState>()(
  persist(
    (set, get) => ({
      fishes: initialStates.fishes,
      addAFish: () => set({ fishes: get().fishes + 1 }),
    }),
    {
      name: 'food-storage',
      getStorage: () => localStorage,
    },
  ),
);

const useHydratedStore = <T extends keyof OmittedFunctionKeys<StoreState>>(key: T)
  : OmittedFunctionKeys<StoreState>[T] => {
  const [state, setState] = useState(initialStates[key]);
  const zustandState = usePersistedStore((persistedState) => persistedState[key]);

  useEffect(() => {
    setState(zustandState);
  }, [zustandState]);

  return state;
};

useHydratedStore is only supposed to be used for getters in the case there is a hydration mismatch

const fishes = useHydratedStore('fishes');

It is used differently than useStore is, in that you pass a string instead of a function, which isn't ideal, but it works for me for now as a fully type-safe solution.

@ShahriarKh
Copy link

ShahriarKh commented Feb 8, 2023

Another simple workaround:

function component() {
    const bears = useStore(state => state.bears)
    const [loaded, setLoaded] = useState(false)

    useEffect(() => {
        setLoaded(true);
    }, [bears]);

    return (
        <div>{loaded ? bears : ""}</div>
    )
}

@codesjedi
Copy link

Thanks @ShahriarKh, your workaround worked for me :)

@Duckinm
Copy link

Duckinm commented Mar 1, 2023

@KGDavidson wrap around with your solution, I do a selector version to bring back as normally use. So now, you can use the bound store without set the useEffect whenever you need the persist item from the store

import { useEffect, useState } from 'react'
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface States {
  count: number
  flag: boolean
}

interface Actions {
  increment: () => void
  decrement: () => void
  toggleFlag: () => void
}

interface Store extends States, Actions {}

const initialStates: States = {
  count: 0,
  flag: false,
}

export const useSetBoundStore = create<Store>()(
  persist(
    (set, get) => ({
      ...initialStates,
      increment: () => set((state) => ({ count: state.count + 1 })),
      decrement: () => set((state) => ({ count: state.count - 1 })),
      toggleFlag: () => set((state) => ({ flag: !state.flag })),
    }),
    {
      name: 'persist-store',
    }
  )
)

export const useBoundStore = <T extends keyof States>(selector: (state: States) => States[T]): States[T] => {
  const [state, setState] = useState(selector(initialStates))
  const zustandState = useSetBoundStore((persistedState) => selector(persistedState))

  useEffect(() => {
    setState(zustandState)
  }, [zustandState])

  return state
}

And in a NextJS file using it like this..

'use client'

import { useBoundStore, useSetBoundStore } from '@/store/use-bound-store'

export default function PokeBallCard() {
  const increment = useSetBoundStore((state) => state.increment)
  const switchPokeball = useSetBoundStore((state) => state.toggleFlag)
  const count = useBoundStore((state) => state.count)
  const flag = useBoundStore((state) => state.flag)

  return (
    <>
      <button type="button" onClick={increment}>
        Click me my pokemon
      </button>
      <button type="button" onClick={switchPokeball}>
        Toggle catcher
      </button>
      <div>{count}</div>
      <div>{flag ? 'yes' : 'no'}</div>
    </>
  )
}

@bobylito
Copy link
Contributor

bobylito commented Mar 2, 2023

What about using router.isReady from useRouter to conditionnally display the component that is using the persisted store data? It “works for me” 😅

@Duckinm
Copy link

Duckinm commented Mar 2, 2023 via email

@bobylito
Copy link
Contributor

bobylito commented Mar 2, 2023

Hasn’t try it yet, do you have a example on that?

On Mar 2, 2023 at 2:30 PM +0700, Alex S @.>, wrote: What about using router.isReady from useRouter? It “works for me” 😅 — Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you commented.Message ID: @.>

Yes! This is what I extracted from my code:

export default function MyComponent() {
  const router = useRouter();
  const { name } = useZustandState();

  return <div>{router.isReady && <p>{name}</p>}</div>
}

@Duckinm
Copy link

Duckinm commented Mar 2, 2023

Hasn’t try it yet, do you have a example on that?

On Mar 2, 2023 at 2:30 PM +0700, Alex S @.>, wrote: What about using router.isReady from useRouter? It “works for me” 😅 — Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you commented.Message ID: _@**.**_>

Yes! This is what I extracted from my code:

export default function MyComponent() {
  const router = useRouter();
  const { name } = useZustandState();

  return <div>{router.isReady && <p>{name}</p>}</div>
}

Oh I see, actually that going to be the similar logic though, however I do think that implementing with useEffect will be a little bit lighter and can use in normal react app too.

Thank for an example!

@shanehoban
Copy link

shanehoban commented Mar 23, 2023

This article is a nice solution to this problem.

The solution is to create your own useStore hook that takes in the store you require to access, and the callback e.g. (state) => state.count

All credit to the author, here is the proposed solution (with an added export for the useStore function):

import { useEffect, useState } from 'react'

export const useStore = <T, F>(
  store: (callback: (state: T) => unknown) => unknown,
  callback: (state: T) => F,
) => {
  const result = store(callback) as F
  const [data, setData] = useState<F>()

  useEffect(() => {
    setData(result)
  }, [result])

  return data
}

Call this function to access state like so:

const store = useStore(useAuthStore, (state) => state)

It works with individual state objects too, and UI updates occur as expected when the state changes:

const fishCount= useStore(useAuthStore, (state) => state.fishCount)

Link to article: https://dev.to/abdulsamad/how-to-use-zustands-persist-middleware-in-nextjs-4lb5

@prenaissance
Copy link

Here's an option using Nextjs' dynamic to lazy-load the target component.
I used it in my project to persist some table filters in localstorage.

const LazyTable = dynamic(
  () => import("@/components/Table"), // component using persisted store
  {
    ssr: false,
  },
);

const MyTable = () => {
  // ...
  return (
    <Flex flexDir="column" alignItems="center" m="1rem">
      <LazyTable
        columns={columns}
        data={profitsQuery.data?.items ?? []}
      />
    </Flex>
  );
}

export default MyTable;

@DavidCodesDev
Copy link

This article is a nice solution to this problem.

The solution is to create your own useStore hook that takes in the store you require to access, and the callback e.g. (state) => state.count

All credit to the author, here is the proposed solution (with an added export for the useStore function):

import { useEffect, useState } from 'react'

export const useStore = <T, F>(
  store: (callback: (state: T) => unknown) => unknown,
  callback: (state: T) => F,
) => {
  const result = store(callback) as F
  const [data, setData] = useState<F>()

  useEffect(() => {
    setData(result)
  }, [result])

  return data
}

Call this function to access state like so:

const store = useStore(useAuthStore, (state) => state)

It works with individual state objects too, and UI updates occur as expected when the state changes:

const fishCount= useStore(useAuthStore, (state) => state.fishCount)

Link to article: https://dev.to/abdulsamad/how-to-use-zustands-persist-middleware-in-nextjs-4lb5

This is a very nice solution, but it introduces a new error, namely it's now undefined on first render just like the comments in that article, how would we tackle this without fixing this each time in an ugly way in each component?

@andre-lergier
Copy link

andre-lergier commented Jun 29, 2023

Using the provided useStore hook solution from @shanehoban I now also get another error.
If I use the store values in multiple components I get the following warning:

Warning: Cannot update a component (`Libraries`) while rendering a different component (`CookieBanner`).
To locate the bad setState() call inside `CookieBanner`, follow the stack trace as described in 
https://reactjs.org/link/setstate-in-render

@AlejandroRodarte
Copy link

AlejandroRodarte commented Aug 12, 2023

You can skip hydration and trigger it manually by calling store.persist.rehydrate(). I do it on a useEffect hook at _app.tsx and got rid of the hydration error. Keep in mind this is SSR/SSG-friendly too.

export default function App({ Component, pageProps }: AppProps) {
  // your zustand store
  const store = useStoreRef(pageProps.preloadedState);

  useEffect(() => {
    // rehydrate zustand store after next.js hydrates
    store.type === 'client' && store.instance.persist.rehydrate();
  }, []);

  return (
    <PokemonContext.Provider value={store}>
      <Component {...pageProps} />
    </PokemonContext.Provider>
  );
}

@7sne
Copy link

7sne commented Aug 16, 2023

Above solutions work (with the caveat of undefined values on the first render). Nonetheless, I think that most of them seem to be a little bloated imho. @AlejandroRodarte proposed a nice solution, I don't get the need to check for store.type and what this variable is to be fair. Would love to know the details tho.

That's my take using context + next app dir.

/store/form

export const createFormStore = () =>
  createStore<FormStore>()(
    persist(
      (set, get) =>
        ({
          // ...state here
        } as FormStore),
      {
        name: "form-store",
        skipHydration: true,
      }
    )
  );

export const formStore = createFormStore();

export const FormContext = createContext<ReturnType<
  typeof createFormStore
> | null>(formStore);

// Use this function to get the state in your app (pardon the any 🥺)
export function useFormStore(selector: (state: FormStore) => any) {
  return useStore(useContext(FormContext)!, selector);
}

/app/providers

type Props = {
  children: React.ReactNode;
};

export function Providers({ children }: Props) {
  const formStoreRef = useRef(formStore);

  useEffect(() => {
    formStore.persist.rehydrate();
  }, []);

  return (
    <FormContext.Provider value={formStoreRef.current}>
      <Toaster />
      <ApolloProvider client={client}>{children}</ApolloProvider>
    </FormContext.Provider>
  );
}

Somewhere in your layout:

      // ...
        <Providers>
          <main className="flex flex-col items-start max-w-6xl min-h-screen mx-auto gap-y-12">
            <OverlayHub />
            {children}
          </main>
        </Providers>
      // ...

@AlejandroRodarte
Copy link

AlejandroRodarte commented Aug 16, 2023

@7sne I apologize for not providing more context to the code I posted.

My next.js app generates two types of stores: (1) a server-side-only store which does not include the persist middleware, and (2) a client-side-only store which includes it.

I did this because using persist during SSR/SSG yields the following warning log:

[zustand persist middleware] Unable to update item 'your-named-storage', the given storage is currently unavailable.

I use store.type === 'client' to know if store.instance.persist is defined and perform re-hydration. store.type is a custom discriminator string I made to distinguish between both types of stores.

const createPokemonStore = {
  onClient: (preloadedState?: Partial<PokemonStateWithoutFunctions>) => {
    const fullStateCreator = createFullStateCreator(preloadedState);
    return {
      type: 'client' as const,
      instance: create<PokemonState>()(
        persist(fullStateCreator, {
          name: 'pokemon-storage',
          skipHydration: true,
        })
      ),
    };
  },
  onServer: (preloadedState?: Partial<PokemonStateWithoutFunctions>) => {
    const fullStateCreator = createFullStateCreator(preloadedState);
    return {
      type: 'server' as const,
      instance: create<PokemonState>()(fullStateCreator),
    };
  },
};
export type PokemonStore = ReturnType<
  typeof getPokemonStore.onClient | typeof getPokemonStore.onServer
>;

@noconsulate
Copy link

I've tried some of these solutions but not all of them because they seem hard and I'm lazy. I guess I'll just have to rework my whole store and see if I can get it to work.

I'd like to use store.persist.rehydrate() but it doesn't seem to do anything! I notice @AlejandroRodarte has the extra word "instance" in his call and maybe that has something to do with // rehydrate zustand store after next.js hydrates.

It'd be nice if there were a straightforward way to do this because as it stands it would seem that persist just doesn't really support Nextjs.

@AlejandroRodarte
Copy link

AlejandroRodarte commented Aug 25, 2023

@noconsulate Skipping hydration definitely works! store.instance is specific to my solution, so don't mind it. However, the idea remains the same: skip zustand re-hydration until next.js hydrates by setting { skipHydration: true } in your store config and calling <your-zustand-store>.persist.rehydrate() inside a useEffect callback. Code you write inside useEffect is sure to run after next.js hydrates.

I made some changes to my original solution; you can find the source code here, but I will explain the main beats of it.

Creating the zustand store

  • So we know that next.js can run both on the server and client, right?
  • This implies we will manage two types of stores: a client-side store, and a server-side store.
  • This distinction is important because I don't want to inject the persist middleware in my server-side store, only on the client-side one.
  • There are infinite amount of solutions to this problem, but mine was to create two helper functions: createPokemonStore.onClient() and createPokemonStore.onServer().
  • Both functions accept as input a preloadedState object, which represents data populated by getStaticProps/getServerSideProps.
  • To be able to distinguish between both types of stores, instead of simply returning the zustand stores directly, I wrap them in a plain, old JavaScript object that comes attached with a unique discriminator string: the store type.
  • type can have one of two values: 'client' for client-side stores, and 'server' for server-side stores.
  • This wrapper object also attaches the actual, real zustand store instance in a property called instance.
// create a zustand store...
const getPokemonStore = {
  // ...for the client
  onClient: (preloadedState?: Partial<PokemonStateWithoutFunctions>) => {
    const fullStateCreator = createFullStateCreator(preloadedState);
    return {
      type: 'client' as const,
      // (1) zustand store will be persisted into local storage so it doesn't dissappear
      // after a page refresh, meaning we require the persist() middleware
      // (2) { skipHydration: true } is required to avoid hydration errors,
      // please visit https://github.com/pmndrs/zustand/issues/938 for more information on it
      instance: create<PokemonState>()(
        persist(fullStateCreator, {
          name: 'pokemon-storage',
          skipHydration: true,
        })
      ),
    };
  },
  // ...for the server: persist() middleware is not needed
  onServer: (preloadedState?: Partial<PokemonStateWithoutFunctions>) => {
    const fullStateCreator = createFullStateCreator(preloadedState);
    return {
      type: 'server' as const,
      instance: create<PokemonState>()(fullStateCreator),
    };
  },
};

Creating a custom hook to manage my zustand store: useStoreRef

Note: This is where things deviated a little bit from my original solution, but the main idea remains the same.

  • useStoreRef is a hook that is assigned with the following tasks
  1. Initialize the zustand store once on the client and once on the server using a ref. Why a ref? To avoid re-renders.
  2. Instead of simply exposing the zustand store directly, I expose an API of sorts (a collection of callback functions) that will allow my components to interact with the zustand store. Three callbacks compose the API: a (1) select function to select data from the store, a (2) rehydrateFromStorage function to rehydrate our zustand store once, and (3) a rehydrate callback to perform a manual re-hydration whenever the developer needs to.
export interface UseStoreRefApi {
  select: <T>(selector: (state: PokemonFullState) => T) => T;
  storage: {
    rehydrate: () => void;
  };
  rehydrate: (partialState: Partial<PokemonState>) => void;
}

// (1) hook that creates a zustand store instance and saves it into a ref
// (2) why a ref? to avoid triggering unnecessary re-renders
// (3) this hook is called in BOTH server-side and client-side
const useStoreRef = (
  preloadedState?: Partial<PokemonStateWithoutFunctions>
): UseStoreRefApi => {
  // get factory depending on context (server or client)
  const createStore = IS_SERVER
    ? createPokemonStore.onServer
    : createPokemonStore.onClient;

  // (1) initialize server-side and first client-side store instance once
  // with the same, pre-loaded state (to avoid hydration errors)
  // (2) preloadedState comes from _app.tsx (pageProps.preloadedState)
  // (3) refs are preferred as they don't trigger re-renders when mutated
  const storeRef = useRef<PokemonStore>();
  if (!storeRef.current) storeRef.current = createStore(preloadedState);

  // one-shot flag to hydrate store from storage only once
  const hasRehydratedFromStorage = useRef(false);

  // api method 1: select a piece of state data from the store ref via a selector
  const select = useCallback(
    <T>(selector: (state: PokemonFullState) => T) =>
      storeRef.current!.instance(selector),
    []
  );

  // (1) api method 2: hydrate client-side store with local-storage data
  // (2) hydration must only be executed once, hence the hasRehydratedFromStorage flag
  const rehydrateFromStorage = useCallback(() => {
    if (
      storeRef.current &&
      storeRef.current.type === 'client' &&
      !hasRehydratedFromStorage.current
    ) {
      storeRef.current.instance.persist.rehydrate();
      hasRehydratedFromStorage.current = true;
    }
  }, []);

  // (1) api method 3: re-hydrate client-side store with some partial state data
  // (2) this is useful to override current state data with page-specific data that
  // was fetched either from getStaticProps/getServerSideProps or through a client-side
  // external API request
  const rehydrate = useCallback((partialState: Partial<PokemonState>) => {
    if (storeRef.current)
      storeRef.current.instance.getState().rehydrate({
        ...storeRef.current.instance.getState(),
        ...partialState,
      });
  }, []);

  // this hook hides our zustand store and just exposes an api to interact with
  return { select, storage: { rehydrate: rehydrateFromStorage }, rehydrate };
};

The useRef that contains our zustand store is of type PokemonStore, which is a union type of both data types that could be returned by either calling createPokemonStore.onServer or createPokemonStore.onServer. Remember that this is all code that can run on both sides!

export type PokemonServerStore = ReturnType<typeof createPokemonStore.onServer>;
export type PokemonClientStore = ReturnType<typeof createPokemonStore.onClient>;

// zustand store type definition, which varies depending on the setting (client or server)
// store.type being 'client' or 'server' serves as a discriminator to distinguish between
// the two versions of this store
export type PokemonStore = PokemonServerStore | PokemonClientStore;

Using useStoreRef at _app.tsx

  • The idea is quite simple: get my custom zustand store API from useStoreRef and inject it into a PokemonContext I had beforehand.
  • pageProps.preloadedState represents the data populated by our getStaticProps/getServerSideProps functions we define in our page components
export default function App({ Component, pageProps }: AppProps) {
  // get store api
  const api = useStoreRef(pageProps.preloadedState);

  // zustand context injected with store api: all next.js pages now have access to it
  return (
    <PokemonContext.Provider value={api}>
      <Component {...pageProps} />
    </PokemonContext.Provider>
  );
}
  • Below you will find code related to the PokemonContext definition and a custom usePokemonStore hook that simply checks the context is defined.
// our zustand context, which simply holds what's returned by
// useStoreRef() at _app.tsx, which is an API to interact with
// the zustand store
export const PokemonContext = createContext<UseStoreRefApi | null>(null);
const usePokemonStore = () => {
  const api = useContext(PokemonContext);
  if (!api) throw new Error('context not found');
  return api;
};

Populating our zustand store with server-side data

  1. Fetch the data you need.
  2. Create a short-lived, server-side-only zustand store.
  3. Set all of the data you fetched into that store.
  4. Get a snapshot of the current state.
  5. Return it as the preloadedState.
// getServerSideProps can populate some zustand state data
export const getServerSideProps: GetServerSideProps<{
  preloadedState: Partial<PokemonStateWithoutFunctions>;
}> = async () => {
  const response = await fetch(
    'http://jherr-pokemon.s3.us-west-1.amazonaws.com/index.json'
  );
  const pokemons = (await response.json()) as Pokemon[];

  // create a short-lived, server-side-only zustand store
  const pokemonStore = createPokemonStore.onServer().instance;

  // set raw pokemon list on store
  pokemonStore.getState().setPokemons(pokemons);

  // get current state snapshot
  const pokemonState = pokemonStore.getState();

  // (1) populate props.preloadedState with server-side-fetched data
  // (2) remember, this is grabbed by _app.tsx at pageProps.preloadedState
  // and is used by useStoreRef() to create the both server and client zustand stores
  // (3) the server zustand store is used for SSR/SSG generation
  // (4) the client zustand store is used for app state management which comes
  // with pre-loaded data from the server in this case
  return {
    props: {
      preloadedState: {
        pokemons: pokemonState.pokemons,
      },
    },
  };
};

Using usePokemonStore in our page components to select data and perform storage re-hydration

  • Inside the useEffect code of your page, call the rehydrateFromStorage API callback to synchronize our zustand store data with the storage system data. Remember that rehydrateFromStorage realizes the actual <your-zustand-store>.persist.rehydrate() call, and it will do it just once.
interface HomeProps {
  preloadedState: Partial<PokemonStateWithoutFunctions>;
}

export default function Home(props: HomeProps) {
  const api = usePokemonStore();
  const {
    select,
    storage: { rehydrate: rehydrateFromStorage },
    rehydrate,
  } = api;

  // (1) access zustand store non-computed and computed properties with api.select()
  // (2) api.select() runs in BOTH server and client
  // (3) in server, api.select() grabs the server-side zustand store and is used
  // for SSR/SSG generation
  // (4) in client, api.select() grabs the client-side zustand store and is used
  // for dynamic state management
  const filteredPokemons = select((state) => state.filteredPokemons);
  const filter = select((state) => state.filter);
  const setFilter = select((state) => state.setFilter);

  useEffect(() => {
    // when page loads, rehydrate our store with local-storage data (in reality, this occurs once)
    rehydrateFromStorage();
    // (1) rehydrate our store with props.preloadedState, which
    // is data provided by getServerSideProps/getStaticProps
    // (2) instead of passing props directly, you can make client-side requests to external APIs
    // which eventually lead to a state-like object that should override current state data
    // (3) since in this case we are fetching our updated data in getServerSideProps, we can
    // simply use props.preloadedState to perform this overriding
    rehydrate(props.preloadedState);
  }, [rehydrateFromStorage, rehydrate, props.preloadedState, removeMainClass]);

  return (
    <div className={mainClasses}>
      <Head>
        <title>Pokemon</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.png" />
      </Head>
      <div>
        <Link href="/count">Count</Link>
        <input
          type="text"
          value={filter}
          onChange={(e) => setFilter(e.target.value)}
          className={styles.search}
        />
      </div>
      <div className={styles.container}>
        {filteredPokemons.slice(0, 20).map((pokemon) => (
          <div key={pokemon.id} className={styles.image}>
            <img
              alt={pokemon.name}
              src={`http://jherr-pokemon.s3.us-west-1.amazonaws.com/${pokemon.image}`}
            />
            <h2>{pokemon.name}</h2>
          </div>
        ))}
      </div>
    </div>
  );
}

Let me know if you more questions about my solution; you may even come up with a much more elegant one. You may also download the repository I linked before to see it in action and without errors for yourself.

On that same repo I have branches that implement the same behavior with other state management systems (redux, mobx, and jotai), in case you are interested.

@noconsulate
Copy link

@AlejandroRodarte Wow thanks for the detailed writeup. I hope many people see this!

I have two stores, one of them works the way you describe here so I can try rehydrate() on that one.

I had given up on persist and just threw in a 'beforeunload' event listener to prevent refresh but I think it's worth making this work.

@AlejandroRodarte
Copy link

AlejandroRodarte commented Aug 25, 2023

@noconsulate No problem man! Glad you liked it.

I would say it's worth it. With this, you can have global state management without losing SSR/SSG perks.

The reason I didn't like the solutions above was not the complexity, but the initial null|undefined values you need to set before hydration, killing any potential of SSR/SSG in those sections.

If you have available the source code where your issue is, feel free to share it so I can help you in more detail.

@joeynguyen
Copy link

joeynguyen commented Sep 10, 2023

It looks like @AlejandroRodarte did a lot of working figuring out a solution that works well, and I applaud him for it. I just wanted to chime in with what's currently working for me to handle the initial undefined store issue and it's much simpler, although it may not work as well as his for all use cases.

I just wanted to mention it here to to offer an alternative and also get others' feedback in case they see pitfalls with it that I haven't considered. It's implemented starting with the official guide's instructions, and does a check for the store to not be undefined before rendering data that depends on it.

...
// yourComponent.tsx

import useStore from './useStore'
import { useBearStore } from './stores/useBearStore'

export default function YourParentComponent() {
  const bearStore = useStore(useBearStore, (state) => state);
  
  // `store` check is required because this is server-rendered by Next.js
  if (!bearStore) return null;

  return (
    <ChildComponent bearStore={bearStore} />
  );
}

@nmatynia
Copy link

nmatynia commented Oct 9, 2023

I also had a problem with this issue but in a app router and I resolved it like this:

  • I added skipHyrdation: true to persist function
export const useBoundStore = create(
  persist(
    () => ({
      count: 0,
      // ...
    }),
    {
      // ...
      skipHydration: true,
    }
  )
  • Then in my page.tsx I added this logic:
export default function Home() {
  const {count} = useBoundStore ();
  const [hasHydrated, setHasHydrated] = useState(false);

  // Rehydrate the store on page load
  useEffect(() => {
    useBoundStore.persist.rehydrate();
    setHasHydrated(true);
  }, []);

  if (!hasHydrated) return null;

  return (
    <div>{count}</div>
  );
}

@benyamynbrkyc
Copy link

benyamynbrkyc commented Nov 15, 2023

I also had a problem with this issue but in a app router and I resolved it like this:

  • I added skipHyrdation: true to persist function
export const useBoundStore = create(
  persist(
    () => ({
      count: 0,
      // ...
    }),
    {
      // ...
      skipHydration: true,
    }
  )
  • Then in my page.tsx I added this logic:
export default function Home() {
  const {count} = useBoundStore ();
  const [hasHydrated, setHasHydrated] = useState(false);

  // Rehydrate the store on page load
  useEffect(() => {
    useBoundStore.persist.rehydrate();
    setHasHydrated(true);
  }, []);

  if (!hasHydrated) return null;

  return (
    <div>{count}</div>
  );
}

Simple and effective. Might not be the most robust solution but definitely works as a quick workaround.

I'll just add that if using TypeScript, you might need to call rehydrate like this: void useCartStore.persist.rehydrate();

@Kais3rP
Copy link

Kais3rP commented Mar 12, 2024

Hi, by reading several solutions proposed by the users and the official Zustand docs, I came up with this solution to handle in a clean way the SSR mismatch issue when using Zustand+NextJS:

// store.js

const initialState = { ... }

let store = (set, get) => ({ ...initialState })
store = immer(store);
store = persist(store, {
	name: 'dashboard-store',
	partialize: (state) => ({
	tokens: state.tokens
		...
	}),
	storage: createJSONStorage(() => localStorage),
});

const useDashboardStore = create(store);
const useSSRDashboardStore = withSSR(useDashboardStore, initialState);

/* Atomic selectors */

/* Persisted */

/* Persisted store hooks return a tuple of [state, hasHydrated] so the result of the call has to be destructured */

export const useTokens = () => useSSRDashboardStore((state) => state.tokens);

The atomic selectors might use both the normal store or the SSR version. The SSR version is produced using this HOF:

// withSSR.js

/* HOF that allows to use a persisted store on SSR since it doesn't return directly the store value, but a duplicate react state valorized after the rendering on client has completed */

export const withSSR = (useStore, initialState) => (selector) => {
	const [value, setValue] = useState(selector(initialState));
	const [hasHydrated, setHasHydrated] = useState(false);

	const hydratedValue = useStore(selector);

	useEffect(() => {
		startTransition(() => {
			setValue(hydratedValue);
			setHasHydrated(true);
		});
	}, [hydratedValue]);

	return [value, hasHydrated];
};

Then in a React component, when subscribing to a part of the state that is persisted, you just need to remember to use destructuring:

// Tokens.js

...

const [tokens, hasHydratedTokens] = useTokens()

Other methods used to check the hydration didn't work for me in NextJS since Zustand hydrates before NextJS.

@julianklumpers
Copy link

julianklumpers commented Mar 18, 2024

Im using this approach to manually hydrate multiple stores.

import React from 'react';
import { Mutate, StoreApi } from 'zustand';

export const useHydrateStores = <S extends Array<Mutate<StoreApi<any>, [['zustand/persist', any]]>>>(...stores: S) => {
  const [hasHydrated, setHasHydrated] = React.useState(stores.length === 0);

  React.useEffect(() => {
    if (!hasHydrated && stores.length > 0) {
      let promises: Promise<boolean>[] = [];
      const subscriptions: (() => void)[] = [];

      stores.forEach(store => {
        promises.push(
          new Promise(r => {
            subscriptions.push(store.persist.onFinishHydration(() => r(true)));
          }),
        );

        store.persist.rehydrate();
      });

      Promise.all(promises).then(res => setHasHydrated(res.every(Boolean)));

      return () => {
        promises = [];
        subscriptions.forEach(unsub => unsub());
      };
    }
  }, [hasHydrated, stores]);

  return hasHydrated;
};

You must set skipHydration: true in your persist stores.
Then in your _app.tsx or layout.tsx use this hook and show a loader when hydrating the stores.

const App = ({ Component, pageProps }: AppPropsWithLayout) => {
  const hasHydrated = useHydrateStores(useStoreOne, useStoreTwo, ...moreStores);

  if (!hasHydrated) {
    return <PageLoader />;
  }

  const getLayout = Component.getLayout || (page => page);

  return (
    <RootLayout title="My Site">
      {getLayout(<Component {...pageProps} />)}
    </RootLayout>
  );
};

@netgfx
Copy link

netgfx commented Apr 5, 2024

I have tried the solution with the useEffect, however it seems really slow, I get whole seconds of the rest of the UI being shown and then the useEffect kicks in and adds the data which finally render the associated component. However if I just load the data straight from the zustand store without useEffect, I get the component to render right away correctly, the only thorn here are the hydration errors...
I haven't tried lazy loading the components but I guess that would be slow as well

@dbritto-dev
Copy link
Collaborator

@netgfx if both server and client doesn't render the same thing on the first render then you face hydration errors that's why you can avoid having a loading state

@agopwns
Copy link

agopwns commented Apr 8, 2024

@ShahriarKh
Thanks. This answer solved my problem really quickly ! 🙌

@Sid-Turner-Ellis
Copy link

Sid-Turner-Ellis commented Apr 23, 2024

I don't get the initial value as undefined if I call hasHydrated e.g.

  const router = useRouter();
  const activeOrgId = useOrganisationStore((store) => store.activeOrgId);

  if (!activeOrgId && useOrganisationStore.persist.hasHydrated()) {
    router.replace("/playground/site/select");
  }

  return (
    <div>
      <div> layout</div>
      {children}
    </div>
  );```

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