quadroid
quadroid

Reputation: 8941

React Hook does not work properly on the first render in gatsby production mode

I have the following Problem:

I have a gatsby website that uses emotion for css in js. I use emotion theming to implement a dark mode. The dark mode works as expected when I run gatsby develop, but does not work if I run it with gatsby build && gatsby serve. More specifically the dark mode works only after switching to light and back again.

I have to following top level component which handles the Theme:

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

The getInitalIsDark function checks a localStorage value, the OS color scheme, and defaults to false. If I run the application, and activate the dark mode the localStorage value is set. If i do now reload the Application the getInitialIsDark method returns true, but the UI Renders the light Theme. Switching back and forth between light and dark works as expected, just the initial load does not work.

If I replace the getInitialIsDark with true loading the darkMode works as expected, but the lightMode is broken. The only way I got this to work is to automatically rerender after loading on time using the following code.

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

But this causes an ugly flicker on page load.

What am I doing wrong with the hook in the first approach, that the initial value is not working as I expect.

Upvotes: 4

Views: 2077

Answers (4)

Waleed
Waleed

Reputation: 69

This is what worked for me, try this and let me know if it works out.

First

In src/components/ i've created a component 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>
        )
      }
    }

Second

Created a gatsby-browser.js

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

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

Third

I've created a ThemeContext.js file in src/context/

    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 }

This should work for you here is the reference I followed from the official Gatsby site

Upvotes: -1

locrizak
locrizak

Reputation: 12281

I had a similar issue where some styles weren't taking effect because they were being applied to through classes which were set on mount (like you only on production build, everything worked fine in develop).

I ended up switching the hydrate function React was using from ReactDOM.hydrate to ReactDOM.render and the issue disappeared.

// gatsby-browser.js
export const replaceHydrateFunction = () => (element, container, callback) => {
  ReactDOM.render(element, container, callback);
};

Upvotes: 1

Richard Matsen
Richard Matsen

Reputation: 23463

@PedroFilipe is correct, useState(() => getInitialIsDark()) is not the way to invoke the checking function on start-up. The expression () => getInitialIsDark() is truthy, so depending on how <ThemedLayout isDark={isDark}> uses the prop it might work by accident, but useState will not evaluate the fuction passed in (as far as I know).

When using an initial value const [myValue, setMyValue] = useState(someInitialValue) the value seen in myValue can be laggy. I'm not sure why, but it seems to be a common cause of problems with hooks.

If the component always renders multiple times (e.g something else is async) the problem does not appear because in the second render the variable will have the expected value.

To be sure you check localstorage on startup, you need an additional useEffect() which explicitly calls your function.

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

Although most useEffect examples use an anonymous function, you might find more understandable to use named functions (following the clean-code principle of using function names for documentation)

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

Upvotes: 2

Pedro Filipe
Pedro Filipe

Reputation: 1075

Did you try to set your initial state like this?

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

Notice that I am not wrapping getInitialIsDark() in an additional function:

useState(() => getInitialIsDark())

You will probably crash your build because localStorage is not defined at buildtime. You might need to check if that exists inside getInitialIsDark.

Hope this helps!

Upvotes: 4

Related Questions