I have a Dark Mode ponent which is a simple toggle between Sun & Moon icons.
DarkMode.tsx
import { observer } from 'mobx-react'
import { MoonIcon, SunIcon } from '@heroicons/react/solid'
import { useStore } from '@/store/index'
export const DarkMode = observer(() => {
const { theme, setTheme, isPersisting } = useStore()
if (!isPersisting) return null
return (
<>
{theme === 'dark' && (
<button
className="fixed bottom-12 right-12 focus:outline-none"
title="Activate light mode"
onClick={() => {
setTheme('light')
}}
>
<MoonIcon className="w-8 h-8" />
</button>
)}
{theme === 'light' && (
<button
className="fixed bottom-12 right-12 focus:outline-none"
title="Activate dark mode"
onClick={() => {
setTheme('dark')
}}
>
<SunIcon className="w-8 h-8" />
</button>
)}
</>
)
})
I am using MobX to track my theme
& mobx-persist-store
to persist the data in localStorage
.
store.ts
import { makeObservable, observable, action } from 'mobx'
import { makePersistable, isPersisting, clearPersistedStore } from 'mobx-persist-store'
import type { Theme, IStore } from '@/types/index'
const name = 'Store'
const IS_SERVER = typeof window === 'undefined'
export class Store implements IStore {
theme: Theme = 'light'
constructor() {
makeObservable(this, {
theme: observable,
setTheme: action.bound,
reset: action.bound,
})
if (!IS_SERVER) {
makePersistable(this, { name, properties: ['theme'], storage: window.localStorage })
}
}
setTheme(theme: Theme) {
this.theme = theme
}
get isPersisting() {
return isPersisting(this)
}
async reset() {
if (!IS_SERVER) await clearPersistedStore(this)
}
}
And I am adding dark
class to html
when the user selectes dark
theme in Dark Mode ponent.
_app.tsx
import React from 'react'
import { AppProps } from 'next/app'
import Head from 'next/head'
import { observer } from 'mobx-react'
import useSystemTheme from 'use-system-theme'
import { useStore } from '@/store/index'
import '@/ponents/NProgress'
import 'nprogress/nprogress.css'
import '@/styles/index.css'
const MyApp = ({ Component, pageProps }: AppProps) => {
const systemTheme = useSystemTheme()
const { theme, setTheme } = useStore()
React.useEffect(() => {
const isDarkTheme = theme === 'dark' || (systemTheme === 'dark' && theme !== 'light')
if (isDarkTheme) {
document.documentElement.classList.add('dark')
setTheme('dark')
} else {
document.documentElement.classList.remove('dark')
setTheme('light')
}
}, [theme, systemTheme])
return (
<>
<Component {...pageProps} />
</>
)
}
export default observer(MyApp)
I am still getting an error that says:
VM356 main.js:16820 Warning: Expected server HTML to contain a matching <button> in <div>.
at button
at wrappedComponent (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624277701361:2690:73)
at Nav (http://localhost:3000/_next/static/chunks/pages/tutorial/the-plete-guide-to-starting-a-blog-in-nextjs-and-mdx.js?ts=1624277701361:12454:23)
at Tutorial (http://localhost:3000/_next/static/chunks/pages/tutorial/the-plete-guide-to-starting-a-blog-in-nextjs-and-mdx.js?ts=1624277701361:12973:24)
at MDXLayout
at http://localhost:3000/_next/static/chunks/pages/tutorial/the-plete-guide-to-starting-a-blog-in-nextjs-and-mdx.js?ts=1624277701361:7880:30
at MDXContent (http://localhost:3000/_next/static/chunks/pages/tutorial/the-plete-guide-to-starting-a-blog-in-nextjs-and-mdx.js?ts=1624277701361:22563:25)
at wrappedComponent (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624277701361:2690:73)
at ErrorBoundary (http://localhost:3000/_next/static/chunks/main.js?ts=1624277701361:767:47)
at ReactDevOverlay (http://localhost:3000/_next/static/chunks/main.js?ts=1624277701361:883:23)
at Container (http://localhost:3000/_next/static/chunks/main.js?ts=1624277701361:8756:5)
at AppContainer (http://localhost:3000/_next/static/chunks/main.js?ts=1624277701361:9244:24)
at Root (http://localhost:3000/_next/static/chunks/main.js?ts=1624277701361:9380:25)
The button
's onClick
event handler disappears from the DOM itself.
Funny thing is it used to work on MacOS but not on Windows. I cloned the same project. What's the issue?
I have a Dark Mode ponent which is a simple toggle between Sun & Moon icons.
DarkMode.tsx
import { observer } from 'mobx-react'
import { MoonIcon, SunIcon } from '@heroicons/react/solid'
import { useStore } from '@/store/index'
export const DarkMode = observer(() => {
const { theme, setTheme, isPersisting } = useStore()
if (!isPersisting) return null
return (
<>
{theme === 'dark' && (
<button
className="fixed bottom-12 right-12 focus:outline-none"
title="Activate light mode"
onClick={() => {
setTheme('light')
}}
>
<MoonIcon className="w-8 h-8" />
</button>
)}
{theme === 'light' && (
<button
className="fixed bottom-12 right-12 focus:outline-none"
title="Activate dark mode"
onClick={() => {
setTheme('dark')
}}
>
<SunIcon className="w-8 h-8" />
</button>
)}
</>
)
})
I am using MobX to track my theme
& mobx-persist-store
to persist the data in localStorage
.
store.ts
import { makeObservable, observable, action } from 'mobx'
import { makePersistable, isPersisting, clearPersistedStore } from 'mobx-persist-store'
import type { Theme, IStore } from '@/types/index'
const name = 'Store'
const IS_SERVER = typeof window === 'undefined'
export class Store implements IStore {
theme: Theme = 'light'
constructor() {
makeObservable(this, {
theme: observable,
setTheme: action.bound,
reset: action.bound,
})
if (!IS_SERVER) {
makePersistable(this, { name, properties: ['theme'], storage: window.localStorage })
}
}
setTheme(theme: Theme) {
this.theme = theme
}
get isPersisting() {
return isPersisting(this)
}
async reset() {
if (!IS_SERVER) await clearPersistedStore(this)
}
}
And I am adding dark
class to html
when the user selectes dark
theme in Dark Mode ponent.
_app.tsx
import React from 'react'
import { AppProps } from 'next/app'
import Head from 'next/head'
import { observer } from 'mobx-react'
import useSystemTheme from 'use-system-theme'
import { useStore } from '@/store/index'
import '@/ponents/NProgress'
import 'nprogress/nprogress.css'
import '@/styles/index.css'
const MyApp = ({ Component, pageProps }: AppProps) => {
const systemTheme = useSystemTheme()
const { theme, setTheme } = useStore()
React.useEffect(() => {
const isDarkTheme = theme === 'dark' || (systemTheme === 'dark' && theme !== 'light')
if (isDarkTheme) {
document.documentElement.classList.add('dark')
setTheme('dark')
} else {
document.documentElement.classList.remove('dark')
setTheme('light')
}
}, [theme, systemTheme])
return (
<>
<Component {...pageProps} />
</>
)
}
export default observer(MyApp)
I am still getting an error that says:
VM356 main.js:16820 Warning: Expected server HTML to contain a matching <button> in <div>.
at button
at wrappedComponent (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624277701361:2690:73)
at Nav (http://localhost:3000/_next/static/chunks/pages/tutorial/the-plete-guide-to-starting-a-blog-in-nextjs-and-mdx.js?ts=1624277701361:12454:23)
at Tutorial (http://localhost:3000/_next/static/chunks/pages/tutorial/the-plete-guide-to-starting-a-blog-in-nextjs-and-mdx.js?ts=1624277701361:12973:24)
at MDXLayout
at http://localhost:3000/_next/static/chunks/pages/tutorial/the-plete-guide-to-starting-a-blog-in-nextjs-and-mdx.js?ts=1624277701361:7880:30
at MDXContent (http://localhost:3000/_next/static/chunks/pages/tutorial/the-plete-guide-to-starting-a-blog-in-nextjs-and-mdx.js?ts=1624277701361:22563:25)
at wrappedComponent (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624277701361:2690:73)
at ErrorBoundary (http://localhost:3000/_next/static/chunks/main.js?ts=1624277701361:767:47)
at ReactDevOverlay (http://localhost:3000/_next/static/chunks/main.js?ts=1624277701361:883:23)
at Container (http://localhost:3000/_next/static/chunks/main.js?ts=1624277701361:8756:5)
at AppContainer (http://localhost:3000/_next/static/chunks/main.js?ts=1624277701361:9244:24)
at Root (http://localhost:3000/_next/static/chunks/main.js?ts=1624277701361:9380:25)
The button
's onClick
event handler disappears from the DOM itself.
Funny thing is it used to work on MacOS but not on Windows. I cloned the same project. What's the issue?
Share Improve this question asked Jun 21, 2021 at 12:45 deadcoder0904deadcoder0904 8,76318 gold badges86 silver badges208 bronze badges3 Answers
Reset to default 6I has the same issue during dark/light theme implementation, but solved it based on deadcoder0904's ment.
In my case, the app was built using next-themes
library.
As this error is related to SSR, it's needed to confirm the ponent is mounted on the frontend side.
import { useEffect, useState } from 'react';
import { useTheme } from 'next-themes';
const ThemeToggler = () => {
const { theme, setTheme } = useTheme()
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => setHasMounted(true));
// this line is the key to avoid the error.
if (!hasMounted) return null;
return (
<div>
The current theme is: {theme}
<button onClick={() => setTheme('light')}>Light Mode</button>
<button onClick={() => setTheme('dark')}>Dark Mode</button>
</div>
)
}
export default ThemeToggler;
Hope this helps you.
On the server your DarkMode
ponent does not render anything (because isPersisting
is false). And then on the client it renders something on the first pass (isPersisting
bees true on the client render) and that is why React (not Next.js) plains that markup between SSR and CSR does not match.
Basically it means that you always need to render some theme with SSR, but SSR does not know about localStorage
so it can only pick the default value. And then correct value will be picked from localStorage
after client render.
If you want to render correct theme with SSR without flashing of old theme or without errors like that one then you need to store it in cookies.
The missing piece of the puzzle was I had wrapped my Nav
outside of ThemeProvider
.
Nav
contained DarkMode
so it couldn't access ThemeProvider
. My _document.tsx
looked like:
<Nav />
<ThemeProvider attribute="class" themes={['light', 'dark']}>
<Component {...pageProps} />
</ThemeProvider>
So I had to bring that Nav
inside ThemeProvider
to get it working.
<ThemeProvider attribute="class" themes={['light', 'dark']}>
<Nav />
<Component {...pageProps} />
</ThemeProvider>