pacocoursey / next-themes

Perfect Next.js dark mode in 2 lines of code. Support System preference and any other theme with no flashing
https://next-themes-example.vercel.app/
MIT License
4.68k stars 169 forks source link

The way avoid hydration errors has a negative impact on SEO. #289

Closed minjunkyeong closed 2 months ago

minjunkyeong commented 2 months ago

To avoid hydration errors, the example returns null until it is mounted, and this is not an appropriate approach for seo.

Avoid Hydration Mismatch

import { useState, useEffect } from 'react'
import { useTheme } from 'next-themes'

const ThemeSwitch = () => {
  const [mounted, setMounted] = useState(false)
  const { theme, setTheme } = useTheme()

  // useEffect only runs on the client, so now we can safely show the UI
  useEffect(() => {
    setMounted(true)
  }, [])

  if (!mounted) {
    return null
  }

  return (
    <select value={theme} onChange={e => setTheme(e.target.value)}>
      <option value="system">System</option>
      <option value="dark">Dark</option>
      <option value="light">Light</option>
    </select>
  )
}

export default ThemeSwitch

So, if I have to worry about seo, isn't it appropriate to use this library that uses local storage?

minjunkyeong commented 2 months ago

In the end, I implemented theme functionality based on cookies... On every page getServersideProps, 'theme', 'themeSource' cookies must be parsed and passed to DarkModeProvider which used in _app.tsx

// context/DarkModeContext.tsx

import {
  ReactNode,
  createContext,
  useCallback,
  useEffect,
  useMemo,
  useState,
} from 'react';
import { setCookie } from 'cookies-next';
import { useMediaQuery } from '@mui/material';

type DarkModeContextType = ReturnType<typeof useDarkMode>;

const DarkModeContext = createContext<DarkModeContextType | undefined>(
  undefined,
);

// theme source name
const SYSTEM = 'system' as const;
const CUSTOM = 'custom' as const;

// theme
const LIGHT = 'light' as const;
const DARK = 'dark' as const;
const DEFAULT_THEME = LIGHT;

//cookie name
const THEME = 'theme' as const;
const THEME_SOURCE = 'themeSource' as const;

const useDarkMode = ({
  cookieTheme,
  cookieThemeSource,
}: {
  cookieThemeSource: typeof SYSTEM | typeof CUSTOM | null;
  cookieTheme: typeof LIGHT | typeof DARK | null;
}) => {
  const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
  const prefersLightMode = useMediaQuery('(prefers-color-scheme: light)');

  const [theme, setTheme] = useState<typeof LIGHT | typeof DARK>(
    cookieTheme || DEFAULT_THEME,
  );
  const [source, setSource] = useState<typeof SYSTEM | typeof CUSTOM>(
    cookieThemeSource || CUSTOM,
  );

  const darkModeActive = useMemo(() => theme === DARK, [theme]);
  const autoModeActive = useMemo(() => source === SYSTEM, [source]);

  const setValues = useCallback(
    ({
      selectedTheme,
      selectedSource,
    }: {
      selectedTheme: typeof LIGHT | typeof DARK;
      selectedSource: typeof SYSTEM | typeof CUSTOM;
    }) => {
      setCookie(THEME, selectedTheme);
      setCookie(THEME_SOURCE, selectedSource);

      setTheme(selectedTheme);
      setSource(selectedSource);
    },
    [],
  );

  const switchToDarkMode = useCallback(() => {
    setValues({ selectedTheme: DARK, selectedSource: CUSTOM });
  }, [setValues]);
  const switchToLightMode = useCallback(() => {
    setValues({ selectedTheme: LIGHT, selectedSource: CUSTOM });
  }, [setValues]);
  const switchToAutoMode = useCallback(() => {
    setValues({
      selectedTheme: prefersDarkMode ? DARK : LIGHT,
      selectedSource: SYSTEM,
    });
    setTheme(prefersDarkMode ? DARK : LIGHT);
  }, [prefersDarkMode, setValues]);

  // If there is no value saved in cookies when accessing the website for the first time, set the theme according to the user system settings.
  useEffect(() => {
    if (cookieThemeSource === null && cookieTheme === null) {
      switchToAutoMode();
    }
  }, [cookieTheme, cookieThemeSource, switchToAutoMode]);

  // Change theme when changing system theme settings
  useEffect(() => {
    if (
      source === SYSTEM &&
      (prefersDarkMode === true || prefersLightMode === true)
    ) {
      switchToAutoMode();
    }
  }, [prefersDarkMode, prefersLightMode, source, switchToAutoMode]);

  return {
    autoModeActive,
    darkModeActive,
    switchToAutoMode,
    switchToDarkMode,
    switchToLightMode,
    theme,
    cookieThemeSource,
  };
};

const DarkModeProvider = ({
  children,
  cookieTheme,
  cookieThemeSource,
}: {
  cookieThemeSource: typeof SYSTEM | typeof CUSTOM | null;
  cookieTheme: typeof LIGHT | typeof DARK | null;
  children: ReactNode;
}) => {
  const darkModeState = useDarkMode({
    cookieTheme,
    cookieThemeSource,
  });

  return (
    <DarkModeContext.Provider value={darkModeState}>
      {children}
    </DarkModeContext.Provider>
  );
};

export { DarkModeProvider, DarkModeContext };
trm217 commented 2 months ago

Simply put, if we use local-storage, there is no way for the current theme to be rendered, before loading the theme from the local-storage on the client.

Storing the state in a cookie would work, but as of this library does not support other storage solutions other than local-storage as of now.

If we were to allow custom storage solutions (which he have already discussed) this could possible be handled by the library, but as of now, this is out of scope.