6

I have this github repo: https://github.com/salmanfazal01/next-firebase-starter

Basically I am creating a basic boilerplate with NextJS, Firebase and Zustand as a global state manager

I am unable to persist a few states like theme, user, etc in Zustand. After refreshing the window, it defaults back to the default theme.

I did try the persist middleware provided by zustand and even though that works, it causes content mismatch error from the server and client

Error: Hydration failed because the initial UI does not match what was rendered on the server.

How would I go about persisting the theme mode in the state without using other libraries like next-themes?

juliomalves
  • 42,130
  • 20
  • 150
  • 146
Salman Fazal
  • 559
  • 7
  • 22

8 Answers8

5

Desire:

  • You want to persist state when moving between different pages while using server side rendering (SSR) with next.js. For example, you want to show a user's profile picture as you move between pages.

Initial implementation:

  • You save the data into the user's browser's local storage. You are using Zustand to do this.

Problem:

  • You see an error saying that the client-side content does not match the server rendered html. Example from next.js:
# Unhandled Runtime Error

Error: Text content does not match server-rendered HTML. See more info here: [https://nextjs.org/docs/messages/react-hydration-error](https://nextjs.org/docs/messages/react-hydration-error)
  • The reason for this is that you are rendering html on the server with a particular variables (For example, user_name). When you then load your page on the client-side and load the user_name variable from your client-side local storage, this variable contains a different value compared to the server-side. This causes a mismatch, which the error message highlights (https://nextjs.org/docs/messages/react-hydration-error).

Solution:

  • When the client-side html differs, load the variable from the local storage after the component/page has first rendered the html. To solve this with next.js, load your data from local storage only in the useEffect hook (equivalent to onMounted in Nuxt/Vue).
  • Example with Jotai:
// store.js
import { atomWithStorage } from 'jotai/utils'  
  
export const countAtom = atomWithStorage('countAtom', 0);  
// pages/login.js
import {useAtom} from "jotai";  
import {countAtom} from "../stores/store";
import { useEffect, useState} from "react";

export default function Login(props) {  
const [count, setCount] = useAtom(countAtom);   // This gets the data from the store.

const add1 = () => {  
  setCount((count) => count + 1);  
};  

const [displayedCount, setDisplayedCount] = useState();  // Set a local variable.
useEffect(() => {  
  setDisplayedCount(count);  // Set the local variable from our store data.
}, [count])

return (  
    <div >  
      <Head>  
        <title>Login</title>  
      </Head>  

      <main>  
        <p>  
          { displayedCount }
          Hello from the login  
        <button onClick={add1}>Add 1</button>  
        </p>  
      </main>  
    </div>  
)  
}
// pages/index.js
import {useAtom} from "jotai";  
import {countAtom} from "../stores/store";
import { useEffect, useState} from "react";

export default function Home(props) {  
  const [count, setCount] = useAtom(countAtom);   // This gets the data from the store.

  const add1 = () => {  
    setCount((count) => count + 1);  
  };  
  
  const [displayedCount, setDisplayedCount] = useState();  // Set a local variable.
  useEffect(() => {  
    setDisplayedCount(count);  // Set the local variable from our store data.
  }, [count])
 
  return (  
      <div >  
        <Head>  
          <title>Home</title>  
        </Head>  
  
        <main>  
          <p>  
            { displayedCount }
            Hello from the home page
          <button onClick={add1}>Add 1</button>  
          </p>  
  
        </main>  
      </div>  
  )  
}
TD1
  • 374
  • 5
  • 12
4

I ran into similar problem when storing opened state in Zustand for an Advent Calendar site. Each day's state is stored on the client and persisted with Zustand persist. To fix, I just followed the guidance from NextJS to use a Dynamic Import and disable SSR for that component.

import dynamic from 'next/dynamic'

const DynamicHeader = dynamic(() => import('../components/header'), {
  ssr: false,
})

Source: NextJS Dynamic Imports

PaulMest
  • 12,925
  • 7
  • 53
  • 50
2

This error is caused by zustand fetching data from persist middleware storage and updating its store before NextJS hydration complete in the beginning.

This can be simply avoided if you render any zustand store variable after first useEffect hook fired. But if you are using multiple variables inside multiple components then this could be tedious and bad solution.

To avoid it you can create a NextJS Layout and wait till first useEffect hook fire to render child elements.

import { useEffect, useState } from "react"

const CustomLayout = ({ children }) => {

    const [isHydrated, setIsHydrated] = useState(false);

    //Wait till NextJS rehydration completes
    useEffect(() => {
        setIsHydrated(true);
    }, []);

    return (
        <> 
            {isHydrated ? ( <div>{children}</div> ) : <div>Loading...</div>}
        </>
    );
};
export default CustomLayout;

PaulMest
  • 12,925
  • 7
  • 53
  • 50
Mahee Gamage
  • 413
  • 4
  • 12
2

Zustand seems to have a solution for this, im currently using Next 13 app router. i was having similar issues so went to zustand docs, there is property called skipHydration which can be used to control when the hydration occurs.

what i did was , made a client component called Hydrations, and i have imported all my zustand stores and called the rehydrate function , Zustand docs

Auth slice

    import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface TypeAuth {
    admin: TypeAdmin | null;
    setAdmin: (admin: TypeAdmin) => void;
}

const useAuth = create<TypeAuth>()(
    persist(
        (set) => ({
            admin: null,
            setAdmin: (admin) => {
                set({ admin });
            },
        }),
        {
            name: 'auth',
            skipHydration: true,
        }
    )
);

export default useAuth;

Hydration File

'use client';

import { useEffect } from 'react';

import { useAuth } from '@/utils/slices';

export default function Hydrations() {
    useEffect(() => {
        useAuth.persist.rehydrate();
    }, []);

    return null;
}

now just import the hydration file in root layout.

Abdul Kàbéèr
  • 237
  • 1
  • 11
1

in Next.js 13.4, you can just add this code as mentioned in article. I have linked to the article: ZUSTAND ARTICLE

store.ts:

"use client";
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";

export const useBearStore = create(
  persist(
    (set: any, get: any) => ({
      bears: 0,
      addABear: () => set({ bears: get().bears + 1 }),
    }),
    {
      name: "food-storage", // name of the item in the storage (must be unique)
      storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
      skipHydration: true,
    }
  )
);

page.tsx:

"use client";

import { useBearStore } from "@/store";
import React, { useState, useEffect } from "react";

const page = () => {
  //TODO: MUST BE ADD THIS CODE TO REMOVE HYDRATION ERROR::
  useEffect(() => {
    useBearStore.persist.rehydrate();
  }, []);
  function Controls() {
    const increasePopulation = useBearStore((state: any) => state.addABear);
    return (
      <button
        className="py-4 px-2 rounded-md bg-blue-400"
        onClick={increasePopulation}
      >
        one up
      </button>
    );
  }
  const DisplayCounter = () => {
    const counter = useBearStore((state: any) => state.bears);
    return <div>Counter: {counter}</div>;
  };
  return (
    <div>
      <h1>Hello Do you see me</h1>
      {Controls()}
      {DisplayCounter()}
    </div>
  );
};

export default page;
0

I suppose you need to use useEffect() to transfer your Zustand states to useState() to prevent Hydration error.

Your Zustand State Data that you imported in any page / component needs to be updated inside useEffect() to new useState().

https://nextjs.org/docs/messages/react-hydration-error

-1

This error is caused by Nextjs SSR so all we have to do is set isSSR to true meaning we meaning we assume on the start our page is being server-side rendered and when its true return null.... so we use useEffect to set isSSR to false on client-side immediately the page gets mounted. I hope this solves your problem...

import { useState, useEffect } from 'react'

const App = ({ Component, pageProps }: AppProps) => {
const [isSSR, setIsSSR] = useState(true);

useEffect(() => {
setIsSSR(false)
}, [])

if (isSSR) return null;

return (
 <Component {...pageProps} />
)

}

  • This solution is akin to "delete your application to resolve the error". Returning null in SSR is not a solution. You may as well disable SSR entirely. A proper solution will support SSR and a deferred loading of any client-side data – Liam Schauerman Mar 04 '23 at 01:00
  • what return null is doing is simple, we just return null when client is not ready so nothing gets rendered you can try and see it works perfectly for me.. try it out and give me your feedback – delinuxist Mar 12 '23 at 17:54
-1

Zuzstand docs, integrating persisting data with Next js: https://docs.pmnd.rs/zustand/integrations/persisting-store-data#usage-in-next.js

NextJS uses Server Side Rendering, and it will compare the rendered component on the server with the one rendered on the client. But since you are using data from the browser to change your component, the two renders will differ and Next will throw a warning at you.

The errors usually are:

  • Text content does not match server-rendered HTML
  • Hydration failed because the initial UI does not match what was rendered on the server
  • There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering To solve these errors.

create a custom hook so that Zustand waits for a little before changing your components.

Create a file with the following:

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

Now in your pages, you will use the hook a little bit differently:

// useBearStore.ts

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

// the store itself does not need any change
export const useBearStore = create(
  persist(
    (set, get) => ({
      bears: 0,
      addABear: () => set({ bears: get().bears + 1 }),
    }),
    {
      name: 'food-storage',
    }
  )
)
// yourComponent.tsx

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

const bears = useStore(useBearStore, (state) => state.bears)