下一个 js 错误 "Warning: Expected server HTML to contain a matching <button> in <div>"

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

我有一个深色模式组件,它是太阳和月亮图标之间的简单切换。

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>
            )}
        </>
    )
})

我正在使用 MobX 来跟踪我的 theme & mobx-persist-store 以将数据保存在 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)
    }
}

当用户在黑暗模式组件中选择 dark 主题时,我将 dark class 添加到 html

_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 '@/components/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)

我仍然收到一条错误消息:

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-complete-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-complete-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-complete-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-complete-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)

buttononClick 事件处理程序从 DOM 本身消失。

有趣的是,它以前可以在 MacOS 上运行,但不能在 Windows 上运行。我克隆了同一个项目。有什么问题?

在服务器上,您的 DarkMode 组件不呈现任何内容(因为 isPersisting 为 false)。然后在客户端上,它在第一遍渲染了一些东西(isPersisting 在客户端渲染上变为 true),这就是为什么 React(不是 Next.js)抱怨 SSR 和 CSR 之间的标记不匹配。

基本上就是说你总是需要用SSR渲染一些主题,但是SSR不知道localStorage所以它只能选择默认值。然后在客户端渲染后从 localStorage 中选择正确的值。

如果您想使用 SSR 呈现正确的主题而不闪烁旧主题或没有类似的错误,那么您需要将其存储在 cookie 中。

缺少的一块拼图是我把我的 Nav 包裹在 ThemeProvider 外面。

Nav 包含 DarkMode,因此无法访问 ThemeProvider。我的 _document.tsx 看起来像:

<Nav />
<ThemeProvider attribute="class" themes={['light', 'dark']}>
    <Component {...pageProps} />
</ThemeProvider>

所以我不得不将 Nav 放入 ThemeProvider 中以使其正常工作。

<ThemeProvider attribute="class" themes={['light', 'dark']}>
    <Nav />
    <Component {...pageProps} />
</ThemeProvider>