gatsbyreact-ssr

DOM Attributes do not update in Gatsby


I am having a weird issue on the static file after run gatsby build.

DOM's attributes (like className) could not be updated by listening to the prop change, but not the case of DOM's content, like text or DOM's children.

// Verison 1, not working

const ThemeProvider = ({ isLight, children }) => {
  return (
    <div className={isLight ? 'light-theme' : 'dark-theme'}> // <- does not change when `isLight` updating
       <h1>{isLight ? 'light-theme' : 'dark-theme'}</h1> // <- changes when `isLight` updating
      {children}
    </div>
  )
}

// Verison 2, not working

// still having the same issue
const ThemeProvider = ({ isLight, children }) => {
  if (isLight)
    return (
      <div className="light-theme">
       <h1>{isLight ? 'light-theme' : 'dark-theme'}</h1>
        {children}
      </div>
    )
  return (
    <div className="dark-theme">
       <h1>{isLight ? 'light-theme' : 'dark-theme'}</h1>
      {children}
    </div>
  )
}

// Verison 3, working

const ThemeProvider = ({ isLight, children }) => {
  if (isLight)
    return (
      <div className="light-theme">
       <h1>{isLight ? 'light-theme' : 'dark-theme'}</h1>
        {children}
      </div>
    )
  return (
    <section className="dark-theme"> // <-- change to the different DOM, everything works fine
       <h1>{isLight ? 'light-theme' : 'dark-theme'}</h1>
      {children}
    </section>
  )
}

Solution

  • Hard to say without your minimal example, but I think I might know your likely error.

    This will happen when your virtual-DOM on the server (so during SSR) and the virtual-DOM for the hydration (the first run in the browser) is different.

    This can happen if you write some code like this:

    export default function Comp() {
      let test = 1;
      if (typeof window !== undefined) {
        test = 2;
        // usually you'd be using some browser API or
        // reading query params or something here
      }
      return <a href={test}>not updated</a>;
    }
    

    What happens is that the DOM from the server will contain <a href=1>, and then when React/Gatsby is running ReactDOM.hydrate once the browser has loaded, it uses this as the "truth" of what the DOM was when it was rendered on the server.

    The only problem is that ReactDOM.hydrate will get <a href=2> as its result. So when the real ReactDOM.render is run afterwards (doing the so-called reconciliation), the virtual DOM will see that <a href=2> and <a href=2> are the same thing. Even if the actual DOM has <a href=1>. So it won't get updated.


    So how do you fix this? Well, the code above is buggy, you shouldn't write it like that, you can do it in a side-effect, like useEffect or componentDidMount. So that SSR AND the rehydrate will get the same result.

    For our simple example above it could look like:

    export default function Comp() {
      const [test, setTest] = useState();
      useEffect(() => { setTest(2); }, [setTest])
      return <a href={test}>updated</a>;
    }