The latest version of React documentation has been available for a long time and is developed with next.js. Support online editing, diablo and other related functions.

I’m currently developing a wordle game for idioms using react, remix and tailwindcss. One problem encountered was that switching to dark mode in SSR scenarios caused the page to flash.

So how to use dark theme in SSR, so that the page does not flash?

Use dark mode in SSR

The implementation of a dark theme is usually to store the theme variable in localStorage. Read the dark theme variable from localStorage while rendering the page and set the corresponding class to dark.

The current game is developed with remix + tailwindcss. Therefore, the dark mode is also set by referring to the dark of tailwindcss. Such as:

// On page load or when changing themes, best to add inline in `head` to avoid FOUC
if (
  localStorage.theme === 'dark'| | (! ('theme' in localStorage) &&
    window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
  document.documentElement.classList.add('dark')}else {
  document.documentElement.classList.remove('dark')}// Whenever the user explicitly chooses light mode
localStorage.theme = 'light'

// Whenever the user explicitly chooses dark mode
localStorage.theme = 'dark'

// Whenever the user explicitly chooses to respect the OS preference
localStorage.removeItem('theme')
Copy the code

Check if it is dark mode by reading localStorage’s theme variable, or if it is dark mode

document.documentElement.classList.add('dark')
Copy the code

Otherwise the call

document.documentElement.classList.remove('dark')
Copy the code

So the component might look like this (assuming that clicking on the icon to toggle dark and light colors is in the Header component)

import { useState, useEffect } from 'react'

export default function Header() {
  const [isDark, setIsDark] = useState(() = > {
    if (typeof document= = ='undefined') return false
    return document.documentElement.classList.contains('dark')
  })

  useEffect(() = > {
    const theme = localStorage.getItem('theme')
    if (theme === 'dark') {
      // Add dark class
      document.documentElement.classList.add('dark')}}, [])return <header>{isDark ? <Sun /> : <Moon />}</header>
}
Copy the code

If the page is in CSR mode then there is no problem and the page will not flash. But if the page is SSR rendered, there is a glitter problem. Consider this component in two phases:

  • In the SSR phase, isDark is false, so the page has a white background color.
  • In the CSR phase, isDark may be true, so the page has a dark background.

Because of these two phases, if the page has been set to dark mode, the page will have a white background returned by the server rendering -> dark background returned by the client rendering.

So how to solve this flashing problem?

Since the topic variable is stored in localStorage, localStorage variables cannot be used on the server side, only on the client side. Where should the logic for setting dark variables be placed if used on the client side?

  1. inuseEffectThe use of. As is known to alluseEffectIs executed later, after the page has been rendered. So if the code logic is placed inuseEffectIs not executed, will also cause the page to flash.
  2. inuseLayoutEffectThe use of.useLayoutEffectIs executed early, before the page renders the DOM. But the execution of the code ultimately depends on itreactThe code is loaded first, so this time is also relatively lag, will also cause the page flash.

Since useLayoutEffect and useEffect do not work, is dark mode not possible in SSR? The answer is yes.

We just need to execute the script as early as possible to avoid visual flickering. Because SSR returns HTML strings (or stream-rendered buffers), it will eventually be parsed into pages by the browser. Is it possible to avoid flashing by ensuring that the script is executed as early as possible? Take a look at how the React documentation implements dark mode in SSR to avoid page flashing.

Because the React document is developed using next.js, add the following script to the _document file

<script
  dangerouslySetInnerHTML={{
    // Add a self-executing function
    __html: ` (function () { function setTheme(newTheme) { window.__theme = newTheme; if (newTheme === 'dark') { document.documentElement.classList.add('dark'); } else if (newTheme === 'light') { document.documentElement.classList.remove('dark'); } } var preferredTheme; try { preferredTheme = localStorage.getItem('theme'); } catch (err) { } window.__setPreferredTheme = function(newTheme) { preferredTheme = newTheme; setTheme(newTheme); try { localStorage.setItem('theme', newTheme); } catch (err) { } }; var initialTheme = preferredTheme; var darkQuery = window.matchMedia('(prefers-color-scheme: dark)'); if (! initialTheme) { initialTheme = darkQuery.matches ? 'dark' : 'light'; } setTheme(initialTheme); darkQuery.addEventListener('change', function (e) { if (! preferredTheme) { setTheme(e.matches ? 'dark' : 'light'); }}); }) (); `}} / >Copy the code

Because this script is the first element under the body, it is also parsed first. Inside the script is a self-executing function, so after the server returns, the client gets the topic information and sets the topic information during parsing.

The flash of the theme has been solved, so how does the icon switch avoid the flash? This can be done with CSS, tailwindcss supports various presentation forms in dark mode.

<div className="block dark:hidden">
  <button
    type="button"
    aria-label="Use Dark Mode"
    onClick={()= > {
      window.__setPreferredTheme('dark');
    }}
    className="hidden lg:flex items-center h-full pr-2">
    {darkIcon}
  </button>
</div>
<div className="hidden dark:block">
  <button
    type="button"
    aria-label="Use Light Mode"
    onClick={()= > {
      window.__setPreferredTheme('light');
    }}
    className="hidden lg:flex items-center h-full pr-2">
    {lightIcon}
  </button>
</div>
Copy the code

Add block Dark: Hidden on darkIcon and Hidden Dark: Block on lightIcon to make the corresponding icon explicit.

Conclusion:

There are two main ways to use dark mode in SSR mode

  1. Execute the script as early as possible and implement the dark mode through the injected script.
  2. Icon uses CSS to show and hide.

Extension, JS fast implementation of media query

There is a code for this in the React documentation script

var darkQuery = window.matchMedia('(prefers-color-scheme: dark)')
if(! initialTheme) { initialTheme = darkQuery.matches ?'dark' : 'light'
}
setTheme(initialTheme)
darkQuery.addEventListener('change'.function (e) {
  if(! preferredTheme) { setTheme(e.matches ?'dark' : 'light')}})Copy the code

Get the return value of the CSS media query from window.matchMedia, true if true, false otherwise. At the same time, the return value can be obtained in real time by registering listeners.