最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Next js Error "Warning: Expected server HTML to contain a matching <button> in <div&a

programmeradmin4浏览0评论

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 badges
Add a ment  | 

3 Answers 3

Reset to default 6

I 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>
发布评论

评论列表(0)

  1. 暂无评论