Reputation: 2957
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.
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
Reputation: 624
This error is purely because of wrong html structure like bellow:
<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
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
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
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
Reputation: 3062
For ShadCn, the ThemeProvider
tag should be under html
and body
tags
Upvotes: 0
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
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
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
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
On checking the response for the test page, I can see the HTML response
Upvotes: 9