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