gatsby生产模式下React Hook在第一次渲染时无法正常工作

4

我有以下问题:

我有一个使用emotion进行css in js的gatsby网站。我使用emotion主题实现了深色模式。当我运行gatsby develop时,深色模式按预期工作,但是如果我使用gatsby build && gatsby serve运行它,则无法正常工作。具体来说,在切换到浅色和再次切换到深色之后,深色模式才能正常工作。

我有一个顶级组件来处理主题:

const Layout = ({ children }) => {
  const [isDark, setIsDark] = useState(() => getInitialIsDark())

  useEffect(() => {
    if (typeof window !== "undefined") {
      console.log("save is dark " + isDark)
      window.localStorage.setItem("theming:isDark", isDark.toString())
    }
  }, [isDark])

  return (
    <ThemeProvider theme={isDark ? themeDark : themeLight}>
      <ThemedLayout setIsDark={() => setIsDark(!isDark)} isDark={isDark}>{children}</ThemedLayout>
    </ThemeProvider>
  )
}

getInitalIsDark 函数检查 localStorage 值、操作系统的颜色方案,并默认为 false。如果我运行应用程序并激活深色模式,则会设置 localStorage 值。如果不重新加载应用程序,则 getInitialIsDark 方法返回 true,但 UI 仍呈现浅色主题。在浅色和深色之间切换可以正常工作,只有初始加载无法正常工作。

如果我将 getInitialIsDark 更改为 true,则深色模式会按预期工作,但浅色模式会出现问题。唯一让此方法正常工作的方式是使用以下代码在加载后自动重新渲染一次。

const Layout = ({ children }) => {
  const [isDark, setIsDark] = useState(false)
  const [isReady, setIsReady] = useState(false)

  useEffect(() => {
    if (typeof window !== "undefined" && isReady) {
      console.log("save is dark " + isDark)
      window.localStorage.setItem("theming:isDark", isDark.toString())
    }
  }, [isDark, isReady])

  useEffect(() => setIsReady(true), [])
  useEffect(() => {
    const useDark = getInitialIsDark()
    console.log("init is dark " + useDark)
    setIsDark(useDark)
  }, [])

  return (
    <ThemeProvider theme={isDark ? themeDark : themeLight}>
      {isReady ? (<ThemedLayout setIsDark={() => setIsDark(!isDark)} isDark={isDark}>{children}</ThemedLayout>) : <div/>}
    </ThemeProvider>
  )
}

但是这会导致页面加载时出现不美观的闪烁。

在第一种方法中,我的钩子函数有什么问题,导致初始值没有按照我的期望工作?


你如何渲染你的布局提供者?你使用wrapRootElement还是直接在每个页面中添加它? - Shubham Khatri
@ShubhamKhatri 我使用 gatsby-plugin-layout 将所有页面包装到布局提供程序中。 - quadroid
“isReady” 似乎毫无意义 - 你能通过改变第一和第三个 useEffect 的顺序来完成相同的事情吗?而且控制台日志可能会导致闪烁。最好的方法是预取并传递 isDark 的值。 - user12697177
4个回答

4

你是否尝试过像这样设置你的初始状态?

const [isDark, setIsDark] = useState(getInitialIsDark())

请注意,我没有在另一个函数中包装getInitialIsDark():

useState(() => getInitialIsDark())

你可能会因为localStorage在编译时未定义而导致崩溃。你可能需要检查它是否存在于getInitialIsDark中

希望对你有所帮助!


感谢您的建议,我已经直接使用getInitialIsDark进行了测试。如果localStorage在构建时未定义,则不会崩溃,而是像问题中提到的那样默认为false。 - quadroid
2
我重新阅读了问题并看到你提到了那一点。抱歉,是我的错。如果 getInitialIsDark 默认为 false,那么当 Gatsby 构建并找不到 localStorage 时,它是否会返回值 falseundefined?相反,您可以在挂载时在 useEffect 中调用函数 getInitialIsDark,一旦组件挂载,就使用正确的值 "更新" 状态。 - Pedro Filipe

2

@PedroFilipe是正确的,useState(() => getInitialIsDark())不是在启动时调用检查函数的方法。表达式() => getInitialIsDark()是真实的,因此取决于<ThemedLayout isDark={isDark}>如何使用该属性,它可能会意外地工作,但是useState不会评估传递的函数(据我所知)。

当使用初始值const [myValue, setMyValue] = useState(someInitialValue)时,myValue中看到的值可能会滞后。我不确定为什么,但它似乎是钩子问题的常见原因。

如果组件始终多次呈现(例如其他内容是异步的),则不会出现问题,因为在第二次呈现中,变量将具有预期的值。

为了确保在启动时检查localstorage,您需要一个额外的useEffect(),它显式调用您的函数。

useEffect(() => {
  setIsDark(getInitialIsDark());
}, [getInitialIsDark]); //dependency only needed to satisfy linter, essentially runs on mount.

虽然大多数useEffect的例子使用匿名函数,但你可能更容易理解使用命名函数(遵循使用函数名称进行文档化的clean-code原则)。

useEffect(function checkOnMount() {
  setIsDark(getInitialIsDark());
}, [getInitialIsDark]);   

useEffect(function persistOnChange() {
  if (typeof window !== "undefined" && isReady) {
    console.log("save is dark " + isDark)
    window.localStorage.setItem("theming:isDark", isDark.toString())
  }
}, [isDark])

如果我按照描述使用它,我需要引入另一个状态或ref来确保persisitOnChange部分不会使用初始静态值调用。这最终导致了我的第二个解决方案——虽然确实有效,但会导致UI闪烁。也许没有办法在没有闪烁的情况下实现这一点。我已经找到了多篇关于useState(() => ...)的博客文章。看起来它们都是错误的。 - quadroid
这不是关于状态的问题,而是将事件分离到单独的 useEffect() 调用中。关于闪烁问题,最好的选择可能是在 App 中调用 getInitialIsDark() 并将结果作为属性传递。 - Richard Matsen
1
请注意,persistOnChange将在无论有多少其他useEffect的情况下都会运行。您只需要在函数内部解决逻辑问题,或者反过来,为什么您关心localstorage是否更新了相同的值? - Richard Matsen

1
我曾遇到类似的问题,一些样式无法生效,因为它们是通过在挂载时设置的类应用的(例如,仅在生产构建上出现问题,在开发中一切都正常)。
最后,我将React使用的hydrate函数从ReactDOM.hydrate切换到ReactDOM.render,问题就消失了。
// gatsby-browser.js
export const replaceHydrateFunction = () => (element, container, callback) => {
  ReactDOM.render(element, container, callback);
};

这对解决我的问题非常有帮助。我有一个网站,包含一个组件代码分离(在移动端使用X组件,在桌面端使用Y组件)。因为SSR在渲染时无法确定窗口视口大小,所以默认会使用移动的组件。标准的ReactDOM.hydrate方法在桌面端重新渲染时静默但视觉上失败了,因为移动端的HTML存在,而期望的是桌面端的HTML。这个答案解决了我的问题。谢谢! - BU0

-1

这是对我有效的方法,你可以试一下,看看是否奏效。

首先

src/components/ 目录下,我创建了一个名为 navigation.js 的组件。

    export default class Navigation extends Component {
      static contextType = ThemeContext // eslint-disable-line
      render() {
        const theme = this.context  
        return (
          <nav className={'nav scroll' : 'nav'}>
            <div className="nav-container">
               <button
                 className="dark-switcher"
                 onClick={theme.toggleDark}
                 title="Toggle Dark Mode"
                >
              </button>
            </div>
          </nav>
        )
      }
    }

第二个

创建了一个 gatsby-browser.js

    import React from 'react'
    import { ThemeProvider } from './src/context/ThemeContext'

    export const wrapRootElement = ({ element }) => <ThemeProvider>{element}</ThemeProvider>

第三步

我在src/context/中创建了一个ThemeContext.js文件

    import React, { Component } from 'react'

    const defaultState = {
      dark: false,
      notFound: false,
      toggleDark: () => {},
    }

    const ThemeContext = React.createContext(defaultState)

    class ThemeProvider extends Component {
      state = {
        dark: false,
        notFound: false,
      }

      componentDidMount() {
        const lsDark = JSON.parse(localStorage.getItem('dark'))

        if (lsDark) {
          this.setState({ dark: lsDark })
        }
      }

      componentDidUpdate(prevState) {
        const { dark } = this.state

        if (prevState.dark !== dark) {
          localStorage.setItem('dark', JSON.stringify(dark))
        }
      }

      toggleDark = () => {
        this.setState(prevState => ({ dark: !prevState.dark }))
      }

      setNotFound = () => {
        this.setState({ notFound: true })
      }

      setFound = () => {
        this.setState({ notFound: false })
      }

      render() {
        const { children } = this.props
        const { dark, notFound } = this.state

        return (
          <ThemeContext.Provider
            value={{
              dark,
              notFound,
              setFound: this.setFound,
              setNotFound: this.setNotFound,
              toggleDark: this.toggleDark,
            }}
          >
            {children}
          </ThemeContext.Provider>
        )
      }
    }

    export default ThemeContext

    export { ThemeProvider }

这应该适用于您,这是我从官方 Gatsby 网站中遵循的参考。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接