dev_el
dev_el

Reputation: 2957

Nextjs 13 Hydration failed because the initial UI does not match what was rendered on the server

I am using next 13.1.0. I have a ContextProvider that sets a light and dark theme

'use client';
import { Theme, ThemeContext } from '@store/theme';
import { ReactNode, useState, useEffect } from 'react';

interface ContextProviderProps {
  children: ReactNode
}

const ContextProvider = ({ children }: ContextProviderProps) => {
  const [theme, setTheme] = useState<Theme>('dark');

  useEffect(() => {
    const storedTheme = localStorage.getItem('theme');
    if (storedTheme === 'light' || storedTheme === 'dark') {
      setTheme(storedTheme);
    } else {
      localStorage.setItem('theme', theme);
    }
    // added to body because of overscroll-behavior
    document.body.classList.add(theme);
    return () => {
      document.body.classList.remove(theme);
    };
  }, [theme]);

  const toggle = () => {
    const newTheme = theme === 'light' ? 'dark' : 'light';
    setTheme(newTheme);
    localStorage.setItem('theme', newTheme);
  };

  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
};

export { ContextProvider };

I use it in my root layout

import '@styles/globals.scss';
import { GlobalContent } from '@components/GlobalContent/GlobalContent';
import { ContextProvider } from '@components/ContextProvider/ContextProvider';
import { Inter } from '@next/font/google';
import { ReactNode } from 'react';

const inter = Inter({ subsets: ['latin'] });

interface RootLayoutProps {
  children: ReactNode
}

const RootLayout = ({ children }: RootLayoutProps) => {
  return (
    <html lang="en" className={inter.className}>
      <head />
      <body>
        <ContextProvider>
          <GlobalContent>
            {children}
          </GlobalContent>
        </ContextProvider>
      </body>
    </html>
  );
};

export default RootLayout;

And I consume the theme value in my GlobalContent

'use client';
import styles from '@components/GlobalContent/GlobalContent.module.scss';
import { GlobalHeader } from '@components/GlobalHeader/GlobalHeader';
import { GlobalFooter } from '@components/GlobalFooter/GlobalFooter';
import { ThemeContext } from '@store/theme';
import { ReactNode, useContext } from 'react';

interface GlobalContentProps {
  children: ReactNode
}

const GlobalContent = ({ children }: GlobalContentProps) => {
  const { theme } = useContext(ThemeContext);
  return (
    <div className={`${theme === 'light' ? styles.lightTheme : styles.darkTheme}`}>
      <GlobalHeader />
      <div className={styles.globalWrapper}>
        <main className={styles.childrenWrapper}>
          {children}
        </main>
        <GlobalFooter />
      </div>
    </div>
  );
};

export { GlobalContent };

I get the error

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

enter image description here

React docs error link

I don't understand why I am getting this error because I am accessing localStorage inside my useEffect, so I expect the HTML generated on the server to be the same with the client before the first render.

How can I solve this error?

Upvotes: 22

Views: 41083

Answers (9)

Debashis Chowdhury
Debashis Chowdhury

Reputation: 624

This error is purely because of wrong html structure like bellow: enter image description here <a> is nested inside another <a> tag

<ul> is nested inside another <ul> tag

<p> is nested inside another <p> tag

<div> is nested inside <p> tag

After I have cleared the these kinds of wrong nesting, I got rid of this error.

Upvotes: 1

Bash
Bash

Reputation: 74

The root layout is defined at the top level of the app directory and applies to all routes. This layout is required and must contain html and body tags, allowing you to modify the initial HTML returned from the server.

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {/* Layout UI */}
        <main>{children}</main>
      </body>
    </html>
  )
}

and by default, layouts in the folder hierarchy are nested, which means they wrap child layouts via their children prop. You can nest layouts by adding layout.js inside specific route segments (folders). so just make it like this:

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return <section>{children}</section>
}

Upvotes: 0

Marshall Fungai
Marshall Fungai

Reputation: 303

For me, I was using nextjs v14 and nexui v2.

I had to put my providers just inside the body tag on the root component. That solved it for me.

@/app/layout.tsx

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {

  const session = await getServerSession(authOptions)
  return (
    <html lang="en">
      <body className={inter.className}>
        <Providers session={session}>
          {children}
        </Providers>
      </body>

    </html>
  );
}

@/app/providers.tsx

export function Providers({ children, session }: { children: React.ReactNode, session: any }) {
    return (
        <SessionProvider session={session}>
            <NextUIProvider>
                {children}
            </NextUIProvider>
        </SessionProvider>
    );
}

Upvotes: 0

Daniel Okello
Daniel Okello

Reputation: 111

For those who might encounter this issue because of using a popover component, i.e.

  <Popover open={open}>
    <PopoverTrigger>
      ...

This is caused by invalid Html tag nesting. The fix is to use the asChild prop.

Forexample;

  <Popover open={open} onOpenChange={setOpen}>
            <PopoverTrigger asChild>
                <Button
                    variant="outline"
                    size="sm"
                    role="combobox"
                    aria-expanded={open}
                    aria-label="Select a store"
                    className={cn("w-[200px] justify-between", className)}
                >

Credits to AissaSemaoui's input here

Upvotes: 1

Russo
Russo

Reputation: 3062

For ShadCn, the ThemeProvider tag should be under html and body tags

Upvotes: 0

Sher Sanginov
Sher Sanginov

Reputation: 595

ran into similar issue when i was adding this Navbar component into RootLayout in layout.tsx in Next.js 13.

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
       <Navbar />
      <body className={font.className}>
        {children}
      </body>
    </html>
  );
}

I changed above code and instead added Navbar inside body tag and it fixed the issue.

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={font.className}>
        <Navbar />
        {children}
      </body>
    </html>
  );
}

Upvotes: 9

GorvGoyl
GorvGoyl

Reputation: 49480

For Next.js 13, return jsx once the component is mounted


function Component() {
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return <></>;
  // write rest of your code
}

Upvotes: 26

Mehdi Faraji
Mehdi Faraji

Reputation: 3846

I solved the error just by dynamically importing the default export of ContextProvider like this in _app.tsx . I'm also persisting context state in localStorage and it works without a problem .

_app.tsx

import dynamic from "next/dynamic";
 
const TodoProvider = dynamic(
  () => import("@/util/context").then((ctx) => ctx.default),
  {
    ssr: false,
  }
);

export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <TodoProvider>
      <Component {...pageProps} />
    </TodoProvider>
  );
}

context.tsx

import React, {
  useState,
  FC,
  createContext,
  ReactNode,
  useEffect,
} from "react";

export const TodoContext = createContext<TodoContextType | null>(null);

interface TodoProvider {
  children: ReactNode;
}

const getInitialState = () => {
  if (typeof window !== "undefined") {
    const todos = localStorage.getItem("todos");
    if (todos) {
      return JSON.parse(todos);
    } else {
      return [];
    }
  }
};

const TodoProvider: FC<TodoProvider> = ({ children }) => {
  const [todos, setTodos] = useState<ITodo[] | []>(getInitialState);
  const saveTodo = (todo: ITodo) => {
    const newTodo: ITodo = {
      id: Math.random(),
      title: todo.title,
      description: todo.description,
      status: false,
    };
    setTodos([...todos, newTodo]);
  };
  const updateTodo = (id: number) => {
    todos.filter((todo: ITodo) => {
      if (todo.id === id) {
        todo.status = !todo.status;
        setTodos([...todos]);
      }
    });
  };

  useEffect(() => {
    if (typeof window !== "undefined") {
      localStorage.setItem("todos", JSON.stringify(todos));
    }
  }, [todos]);

  return (
    <TodoContext.Provider value={{ todos, saveTodo, updateTodo }}>
      {children}
    </TodoContext.Provider>
  );
};

export default TodoProvider;

Upvotes: 1

dev_el
dev_el

Reputation: 2957

I have made a workaround that solves the issue for now at the cost of giving up SSR.

By using a dynamic import on my ContextProvider, I disable server-rendering and the error is gone. As a bonus, the flashing issue from my default dark theme to my light theme saved on localStorage is gone. But I give up the benefits of SSR. If someone finds a better solution, please do share.

import '@styles/globals.scss';
import { GlobalContent } from '@components/GlobalContent/GlobalContent';
import { Inter } from '@next/font/google';
import dynamic from 'next/dynamic';
import { ReactNode } from 'react';

const inter = Inter({ subsets: ['latin'] });

interface RootLayoutProps {
  children: ReactNode
}

// Fixes: Hydration failed because the initial UI does not match what was rendered on the server.
const DynamicContextProvider = dynamic(() => import('@components/ContextProvider/ContextProvider').then(mod => mod.ContextProvider), {
  ssr: false
});

const RootLayout = ({ children }: RootLayoutProps) => {
  return (
    <html lang="en" className={inter.className}>
      <head />
      <body>
        <DynamicContextProvider>
          <GlobalContent>
            {children}
          </GlobalContent>
        </DynamicContextProvider>
      </body>
    </html>
  );
};

export default RootLayout;

This solution does not disable SSR site wide. I added a new test page with the following code

async function getData() {
  const res = await fetch('https://rickandmortyapi.com/api/character', { cache: 'no-store' });
  if (!res.ok) {
    throw new Error('Failed to fetch data');
  }

  return res.json();
}

export default async function Page() {
  const data = await getData();

  return (
    <main>
      {data.results.map((c: any) => {
        return (
          <p key={c.id}>{c.name}</p>
        );
      })}
    </main>
  );
}

After running npm run build, I can see that the test page is using ssr

enter image description here

On checking the response for the test page, I can see the HTML response

enter image description here

Upvotes: 9

Related Questions