Search code examples
reactjsmaterial-uinext.jsthemesdarkmode

Material-UI Nextjs Integration Theme Toggle


I am attempting to recreate the theme toggle feature that the material-ui website.

My Github Repo: https://github.com/jonnyg23/flask-rest-ecommerce/tree/next-app-migration

So far, I have noticed that material-ui's website uses a cookie called paletteType in order to store the client's theme choice. I understand that a Context Provider should be used to set the cookie, however, my nav-bar implementation has an issue changing themes after the second click of my theme toggle button.

Any help would be greatly appreciated, thank you.

CustomThemeProvider.js:

import React, { createContext, useState } from "react";
import { ThemeProvider } from "@material-ui/core/styles";
import getTheme from "../themes";
import Cookie from "js-cookie";

export const CustomThemeContext = createContext({
  // Set the default theme and setter.
  appTheme: "light",
  setTheme: null,
});

const CustomThemeProvider = ({ children, initialAppTheme }) => {
  // State to hold selected theme
  const [themeName, _setThemeName] = useState(initialAppTheme);

  // Retrieve theme object by theme name
  const theme = getTheme(themeName);

  // Wrap setThemeName to store new theme names as cookie.
  const setThemeName = (name) => {
    // console.log("CustomThemeProvider, SetThemeName", name);
    Cookie.set("appTheme", name);
    _setThemeName(name);
  };

  const contextValue = {
    appTheme: themeName,
    setTheme: setThemeName,
  };

  return (
      <CustomThemeContext.Provider value={contextValue}>
        <ThemeProvider theme={theme}>{children}</ThemeProvider>
      </CustomThemeContext.Provider>
  );
};

export default CustomThemeProvider;

_app.js:

import "../styles/globals.css";
import React, { useContext, useEffect } from "react";
import PropTypes from "prop-types";
import { useAuth0 } from "@auth0/auth0-react";
import Head from "next/head";
import { Provider as NextAuthProvider } from "next-auth/client";
import { makeStyles } from "@material-ui/core/styles";
import CssBaseline from "@material-ui/core/CssBaseline";

import Auth0ProviderWithHistory from "../auth/auth0-provider-with-history";
import CustomThemeProvider, {
  CustomThemeContext,
} from "../context/CustomThemeProvider";

export default function App({ Component, pageProps }) {
  // const { isLoading } = useAuth0();
  const ThemeContext = useContext(CustomThemeContext);

  useEffect(() => {
    // Remove the server-side injected CSS.
    const jssStyles = document.querySelector("#jss-server-side");
    if (jssStyles) {
      jssStyles.parentElement.removeChild(jssStyles);
    }
  }, []);

  return (
    <React.Fragment>
      <Head>
        <meta
          name="viewport"
          content="minimum-scale=1, initial-scale=1, width=device-width"
        />
      </Head>
      <CustomThemeProvider initialAppTheme={ThemeContext.appTheme}>
        <Auth0ProviderWithHistory>
          <NextAuthProvider session={pageProps.session}>
            <CssBaseline />
            <Component {...pageProps} />
          </NextAuthProvider>
        </Auth0ProviderWithHistory>
      </CustomThemeProvider>
    </React.Fragment>
  );
}

App.propTypes = {
  Component: PropTypes.elementType.isRequired,
  pageProps: PropTypes.object.isRequired,
};

ThemeModeToggle.js:

import React, { useContext } from "react";
import { IconButton } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import Brightness5TwoToneIcon from "@material-ui/icons/Brightness5TwoTone";
import Brightness2TwoToneIcon from "@material-ui/icons/Brightness2TwoTone";
import { CustomThemeContext } from "../context/CustomThemeProvider";

const useStyles = makeStyles((theme) => ({
  light: {
    color: theme.palette.secondary.main,
  },
  dark: {
    color: theme.palette.secondary.main,
  },
}));

const ThemeModeToggle = ({ fontSize }) => {
  const classes = useStyles();
  const { appTheme, setTheme } = useContext(CustomThemeContext);
  // console.log("ThemeModeToggle", appTheme);

  const handleThemeChange = (appTheme, setTheme) => {
    if (appTheme === "light") {
      setTheme("dark");
    } else {
      setTheme("light");
    }
  };

  return (
    <IconButton onClick={() => handleThemeChange(appTheme, setTheme)}>
      {appTheme === "light" ? (
        <Brightness5TwoToneIcon fontSize={fontSize} className={classes.light} />
      ) : (
        <Brightness2TwoToneIcon fontSize={fontSize} className={classes.dark} />
      )}
    </IconButton>
  );
};

export default ThemeModeToggle;

Solution

  • Solved, I found the issue!

    It turns out that my page components such as StorefrontIcon and the AppBar colors had to be explicitly added with className={classes.NAME} or with style={{ backroundColor: theme.palette.primary.main }} for example. Both methods are shown below in my nav-bar.js file.

    import React from "react";
    import { AppBar, Grid, Container, Toolbar } from "@material-ui/core";
    import { makeStyles, useTheme } from "@material-ui/core/styles";
    import StorefrontIcon from "@material-ui/icons/Storefront";
    
    import MainNav from "./main-nav";
    import AuthNav from "./auth-nav";
    import ThemeModeToggle from "./ThemeModeToggle";
    import { Desktop, SmallScreen } from "./Responsive";
    import HamburgerMenu from "./HamburgerMenu";
    
    const useStyles = makeStyles((theme) => ({
      root: {
        [theme.breakpoints.down("sm")]: {
          padding: 0,
        },
      },
      icon: {
        color: theme.palette.primary.contrastText,
      },
    }));
    
    const NavBar = () => {
      const classes = useStyles();
      const theme = useTheme();
    
      return (
        <AppBar
          position="static"
          elevation={3}
          style={{ backgroundColor: theme.palette.primary.main }}
        >
          <Toolbar>
            <Container className={classes.root}>
              <Grid container justify="center" alignItems="center" spacing={2}>
                <Desktop>
                  <Grid item xs={1}>
                    <StorefrontIcon fontSize="large" className={classes.icon} />
                  </Grid>
                  <Grid item xs={7}>
                    <MainNav />
                  </Grid>
                  <Grid item xs={3}>
                    <AuthNav />
                  </Grid>
                  <Grid item xs={1}>
                    <ThemeModeToggle fontSize="large" />
                  </Grid>
                </Desktop>
                <SmallScreen>
                  <Grid item xs={2}>
                    <StorefrontIcon fontSize="medium" className={classes.icon} />
                  </Grid>
                  <Grid container item xs={10} justify="flex-end">
                    <HamburgerMenu />
                  </Grid>
                </SmallScreen>
              </Grid>
            </Container>
          </Toolbar>
        </AppBar>
      );
    };
    
    export default NavBar;