pmndrs / zustand

🐻 Bear necessities for state management in React
https://zustand-demo.pmnd.rs/
MIT License
48.37k stars 1.5k forks source link

Wait for Nextjs rehydration before zustand store rehydration #938

Closed MoustaphaDev closed 2 years ago

MoustaphaDev commented 2 years ago

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?

dai-shi commented 2 years ago

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

MoustaphaDev commented 2 years ago

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

dai-shi commented 2 years ago

@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 commented 2 years ago

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

AnatoleLucet commented 2 years ago

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

MoustaphaDev commented 2 years ago

Sure, here's the sandbox @AnatoleLucet.

AnatoleLucet commented 2 years ago

@MoustaphaDev Thanks.

I think this is a dup of #324

MoustaphaDev commented 2 years ago

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

isomorpheric commented 2 years ago

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 commented 2 years ago

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

pixelass commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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 commented 1 year ago

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 commented 1 year ago

Thanks @ShahriarKh, your workaround worked for me :)

Duckinm commented 1 year ago

@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 commented 1 year ago

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

Duckinm commented 1 year ago

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

bobylito commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

@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

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

  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

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

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 commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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

benyamynbrkyc commented 1 year ago

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 commented 8 months ago

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 commented 8 months ago

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 commented 8 months ago

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 commented 8 months ago

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

agopwns commented 7 months ago

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

Sid-Turner-Ellis commented 7 months ago

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>
  );```
hungdoansy commented 4 months ago

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

Thank you for this. From your example, we can show a loading screen for 1-2 seconds and then show the page content, instead of return null. It worked. Something just like this in the page.tsx file

const [isLoading, setLoading] = React.useState(true)

React.useEffect(() => {
    setTimeout(() => {
        setLoading(false)
    }, 1000)
}, [])

if (isLoading) {
    return (
        <LoadingScreen />
    )
}

return (
    ...
)
angelolucas commented 1 month ago

You can address the issue using dynamic imports to disable SSR for the specific component that relies on the Zustand store. Here's how you I made it:

import dynamic from 'next/dynamic';
import { useBearStore } from './store';

const Component = () => {
  const bears = useBearStore((state) => state.bears);

  return <div>{bears}</div>;
};

// Disable SSR for this component
export default dynamic(() => Promise.resolve(Component), { ssr: false });
aabuhijleh commented 3 weeks ago

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

I faced an issue when using this

From from the docs,

// useStore.ts
import { useState, useEffect } from 'react'

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
}

export default useStore

setData(result) should be setData(() => result)

Because if result is a function, it will be called by setState and this will cause unexpected behaviour.