2021-10-14Bruno Fernandes

Dark Theme with SSR (Next.js) and Material UI v5

So, I had a bit of a hard time upgrading this blog to Material UI v5. I followed the migration instructions that the authors suggest but everything was still broken, with mismatched CSS class names between the client and the server. I figured the problem was CSS related but it took a while for me to figure out that the real culprit was the useDarkMode hook which I used to switch between the light and dark theme with SSR, persistence, user preferences fallback support, and no content flash.

Utimately I decided to remove it. It seems it doesn't play well with switching themes now that Material UI has ditched JSS in favor of Emotion.

I ended up implementing a ThemeContext to deal with this, using cookies, inspired by the solution on the MUI docs website, and it seems to have worked well enough, and with no content flash (in my actual code, everything is properly organized in its own file, but for the sake of this post):

1import {
2  createTheme,
3  PaletteMode,
4  useMediaQuery,
5  responsiveFontSizes,
6  ThemeOptions,
7} from '@mui/material';
8import {
9  createContext,
10  useCallback,
11  useContext,
12  useEffect,
13  useMemo,
14  useState,
15} from 'react';
16import { ThemeProvider as MuiThemeProvider } from '@mui/material';
17
18function getThemeOptions(mode: PaletteMode): ThemeOptions {
19  return {
20    // theme options
21    palette: {
22      mode,
23      ...(mode === 'light'
24        ? {
25            // light palette
26          }
27        : {
28            // dark palette
29          }),
30    },
31  };
32}
33
34function buildTheme(mode: PaletteMode) {
35  return responsiveFontSizes(createTheme(getThemeOptions(mode)));
36}
37
38function getCookie(name: string) {
39  const regex = new RegExp(`(?:(?:^|.*;*)${name}*=*([^;]*).*$)|^.*$`);
40  return document.cookie.replace(regex, '$1');
41}
42
43type ThemeContextType = {
44  mode: PaletteMode;
45  setMode: (mode: PaletteMode | null) => void;
46  toggleMode: () => void;
47};
48
49export const ThemeContext = createContext<ThemeContextType>({
50  mode: 'light',
51  setMode: () => {},
52  toggleMode: () => {},
53});
54
55export const ThemeProvider: React.FC = ({ children }) => {
56  const [colorMode, setColorMode] = useState<PaletteMode | null>(null);
57  const prefersDarkMode = useMediaQuery('@media (prefers-color-scheme: dark)');
58  const preferredColorMode: PaletteMode = prefersDarkMode ? 'dark' : 'light';
59
60  useEffect(() => {
61    if (process.browser) {
62      const nextColorMode = getCookie('colorMode') as PaletteMode | string;
63      if (nextColorMode !== 'light' && nextColorMode !== 'dark') {
64        setColorMode(null);
65      } else {
66        setColorMode(nextColorMode);
67      }
68    }
69  }, []);
70
71  useEffect(() => {
72    document.cookie = `colorMode=${colorMode ?? preferredColorMode};path=/;max-age=31536000`;
73  }, [colorMode, preferredColorMode]);
74
75  const toggleMode = useCallback(() => {
76    setColorMode(mode => (mode === 'light' ? 'dark' : 'light'));
77  }, []);
78
79  const theme = useMemo(
80    () => buildTheme(colorMode ?? preferredColorMode),
81    [colorMode, preferredColorMode],
82  );
83
84  return (
85    <ThemeContext.Provider
86      value={{
87        mode: colorMode ?? preferredColorMode,
88        setMode: setColorMode,
89        toggleMode,
90      }}
91    >
92      <MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>
93    </ThemeContext.Provider>
94  );
95};
96
97export const useThemeContext = () => useContext(ThemeContext);

With this, you can just replace the ThemeProvider in _app.tsx, and use the useThemeContext hook to toggle the color mode.

See you in the next one!