Can theme changes be carried over between different pages using Material UI?

I've encountered an issue with MUI 5.14.1 where I'm getting an error every time I attempt to save themes across pages using localStorage. Any suggestions on how to resolve this problem or recommendations on a different approach would be greatly appreciated.

import React, { useState } from "react";
import {
  CssBaseline,
  Button,
  Typography,
} from "@mui/material";
import { ThemeProvider } from "@mui/material/styles";
import { Toaster, toast } from "react-hot-toast";
import { Link } from "react-router-dom"; // Import the Link component from react-router-dom
import styles from "../styles/app.module.css";
import MuteSwitch from "../components/MuteSwitch.js";
import StyledAvatar from "../components/StyledAvatar.js";
import Sidebar from "../components/Sidebar";
import {
  toggleDarkMode,
  handleThemeChange,
  lightTheme,
} from "../utils/themeUtils";

const persistedTheme = JSON.parse(localStorage.getItem("theme")) || lightTheme;
const HomePage = () => {
  const [currentTheme, setCurrentTheme] = useState(persistedTheme); // Define the state variable for the current theme
  const [darkMode, setDarkMode] = useState(false); // Track dark mode state, false = light mode, true = dark mode
  const [userInputColor, setUserInputColor] = useState("#1976d2"); // Default initial color
  const [colorPickerColor, setColorPickerColor] = useState("#1976d2"); // Default initial color

  const saveTheme = (theme) => {
    localStorage.setItem("theme", JSON.stringify(currentTheme));
  };

  const handleColorChange = (event) => {
    setColorPickerColor(event.target.value);
    setUserInputColor(event.target.value);
  };

  const handleDarkModeToggle = () => {
    setDarkMode((prevMode) => !prevMode); // Toggle the dark mode state
    toggleDarkMode(darkMode, setCurrentTheme);
  };

  const createToast = (message) => {
    let toastBackground = currentTheme.palette.primary.main;
    let toastColor = currentTheme.palette.primary.contrastText;
    toast.success(message, {
      style: {
        background: toastBackground,
        color: toastColor,
      },
    });
  };
  const handleNewMessages = () => {
    createToast("You have 3 new messages");
  };

  const onThemeChange = () => {
    //possibly darken color picker color
    const updatedTheme = handleThemeChange(userInputColor);
    setCurrentTheme(updatedTheme);
    saveTheme(updatedTheme);
  };

  return (
    <>
      <Toaster />

      <ThemeProvider theme={currentTheme}>
        <CssBaseline />

        <div className={styles.heading}>
          <Typography variant="h1" component="h1" gutterBottom>
            Home Page
          </Typography>
        </div>

        {/* content */}
        <div className={styles.centeredContent}>
          <Button variant="contained">Pretty Colors</Button>
        </div>
        {/* mute switch */}
        <div className={styles.muteSwitch}>
          <MuteSwitch />
        </div>
        {/* avatar */}
        <Link to="/profile" style={{ textDecoration: "none", color: "inherit" }}>
        <div className={styles.avatar}>
          <StyledAvatar>TS</StyledAvatar>
        </div>
        </Link>

        {/* drawer */}
        <div>
          <Sidebar handleThemeChange={onThemeChange} darkMode={darkMode} handleDarkModeToggle={handleDarkModeToggle} handleNewMessages={handleNewMessages} colorPickerColor={colorPickerColor} handleColorChange={handleColorChange}/>
        </div>
      </ThemeProvider>
    </>
  );
};

export default HomePage;

Error:

Unexpected Application Error!
theme.transitions.create is not a function
TypeError: theme.transitions.create is not a function
    at http://localhost:3000/static/js/bundle.js:11135:35

My attempt to use localStorage to store and retrieve the theme in a new file has resulted in an error when trying to change

const [currentTheme, setCurrentTheme] = useState(lightTheme);
. However, this issue has been resolved now. You can follow my solution, which involves using React Context, or Dewaun Ayers' solution (marked as solution in the replies), which utilizes localStorage.

Here's my solution:

Create a file named "ThemeContext.js" (or similar) with the following content:

// ThemeContext.js

import { createContext, useContext, useState } from "react";
import { createTheme } from "@mui/material/styles";

const ThemeContext = createContext();

export const useThemeContext = () => {
  return useContext(ThemeContext);
};

export const ThemeContextProvider = ({ children }) => {
  const [currentTheme, setCurrentTheme] = useState(lightTheme);

  const handleThemeChange = (color) => {
    const secondaryColor = color; //edit this to be your secondary color
    const newTheme = createTheme({
      palette: {
        primary: {
          main: color,
        },
        secondary: {
          main: secondaryColor,
        },
      },
    });

    setCurrentTheme(newTheme);
  };


  return (
    <ThemeContext.Provider
      value={{ currentTheme, handleThemeChange }}
    >
      {children}
    </ThemeContext.Provider>
  );
};

Import this file into your top-level React file and wrap your router/components in the ThemeContextProvider component. Here's how I implemented it:

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import HomePage from "./pages/HomePage";
import ProfilePage from "./pages/ProfilePage";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { ThemeContextProvider } from "./utils/ThemeContext";

const router = createBrowserRouter([
  {
    path: "/",
    element: <HomePage />,
  },
  {
    path: "/profile",
    element: <ProfilePage />,
  },
]);

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <ThemeContextProvider>
      <RouterProvider router={router} />
    </ThemeContextProvider>
  </React.StrictMode>
);

Finally, update your page components to utilize the variables and functions stored in Context, like so:

import { useThemeContext } from "../utils/ThemeContext";

const HomePage = () => {
  const { currentTheme, handleThemeChange, isDarkMode, toggleDarkMode, colorPickerColor, userInputColor, handleColorChange } = useThemeContext();

  const createToast = (message) => {
    let toastBackground = currentTheme.palette.primary.main;
    let toastColor = currentTheme.palette.primary.contrastText;
    toast.success(message, {
      style: {
        background: toastBackground,
        color: toastColor,
      },
    });
  };
  const handleNewMessages = () => {
    createToast("You have 3 new messages");
  };

  const onThemeChange = () => {
    //possibly darken color picker color
    handleThemeChange(userInputColor);
  };

  return (
    <>
      <Toaster />

      <ThemeProvider theme={currentTheme}>
        <CssBaseline />

        <div className={styles.heading}>
          <Typography variant="h1" component="h1" gutterBottom>
            Home Page
          </Typography>
        </div>

        {/* content */}
        <div className={styles.centeredContent}>
          <Button variant="contained">Pretty Colors</Button>
        </div>
        {/* mute switch */}
        <div className={styles.muteSwitch}>
          <MuteSwitch />
        </div>
        {/* avatar */}
        <Link
          to="/profile"
          style={{ textDecoration: "none", color: "inherit" }}
        >
          <div className={styles.avatar}>
            <StyledAvatar>TS</StyledAvatar>
          </div>
        </Link>

        {/* drawer */}
        <div>
        <Sidebar
            handleThemeChange={onThemeChange}
            isDarkMode={isDarkMode}
            handleDarkModeToggle={toggleDarkMode}
            handleNewMessages={handleNewMessages}
            colorPickerColor={colorPickerColor}
            handleColorChange={handleColorChange}
            currentTheme={currentTheme}
          />
        </div>
      </ThemeProvider>
    </>
  );
};

export default HomePage;

This commit to the project repository showcases all the changes made: https://github.com/AnthonySchneider2000/React-Material-UI-Dynamic-Theme-Changer/commit/5e89229b8ec04e2cc3aed3b7fc7205b1396ee401

Answer №1

It seems like you're making progress with saving the theme to local storage.

The error you mentioned is likely due to one of two reasons:

First, you may not be following the correct process for creating a theme using MUI, which involves using the createTheme function with a valid theme object and then passing it to the ThemeProvider.

Alternatively, you might be encountering issues because certain functions (such as transitions.create) in your theme files are being lost when you stringify the theme object before storing it in local storage.

If it's the latter reason, keep in mind that JSON.stringify() does not support undefined, Function, or Symbol values. These values are either omitted or changed to null during conversion.

To address this issue, consider only storing the theme values in local storage before passing them into createTheme or using a token to represent the selected theme and switch between themes based on that token.

I've put together a demonstration of an alternative method for switching themes while persisting to local storage in this Code Sandbox.

I hope this clarifies things for you!

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Creating a responsive design for mobile apps in Ionic using CSS

I need help with making my Ionic app responsive across all mobile devices. The design on the page was created using CSS, but it is not displaying properly on every device. How can I ensure that it adapts to different screen sizes? <template> <Io ...

I am experiencing an issue with environment variables not appearing in my Context component on Next.js. Should I adjust the Next.js configuration or set up the Context to properly utilize the variables?

Why are Environment Variables working on every component inside /pages but not in my Context component in Next.js? Do I need to do some configuration in Next.js for this? (Note: The Shopcontext.tsx file is using a class component that I obtained from a tu ...

Retrieving Data from all Rows in jQuery DataTables

Is there a way to copy all rows in jQuery DataTables into a JavaScript array by clicking the header checkbox? https://i.sstatic.net/57dTB.png I need to locate where jQuery DataTables store the HTML for the remaining page of rows so that I can manipulate ...

Troubleshooting Vue.js: Why is .bind(this) not behaving as anticipated?

Demo: https://codesandbox.io/s/23959y5wnp I have a function being passed down and I'm trying to rebind the this by using .bind(this) on the function. However, the data that is returned still refers to the original component. What could I be missing h ...

Struggling to securely post data to an Express server by hashing passwords with bcrypt?

I'm currently working on developing an API using Express and Sequelize. Specifically, I am writing a function to create a new user where I utilize bcrypt for password hashing. const createNewUser = (data) => { return new Promise(async (resolve, ...

Transmit information using jQuery to an MVC controller

I am developing an ASP.NET MVC3 application and need to send three pieces of data to a specific action when the user clicks on an anchor tag: <a onclick='sendData(<#= Data1,Data2,Data3 #>)'></a> Here is the javascript function ...

How can the jQuery click() method be utilized?

Currently working on a web scraping project, I have managed to gather some valuable data. However, I am now faced with the challenge of looping through multiple pages. Update: Using nodeJS for this project Knowing that there are 10 pages in total, I atte ...

What is causing the parse error in my CSS code?

Running into a parse error on line 314 of my css (style.css). The error is "} expected css(css-rcurlyexpected)". Even after adding the necessary curly brace, it still doesn't validate. Below is my CSS starting from line 314 till the end: /* CSS code ...

What causes the fixed div to appear when scrolling horizontally?

I have replicated this issue in a live example: http://jsfiddle.net/pda2yc6s When scrolling vertically, a specific div element sticks to the top. However, if the window is narrower than the wrapper's width and you scroll horizontally, the sticky elem ...

Troubleshooting Jasmine Unit Testing issues with the ng-select library

Recently, I integrated the ng-select component from Github into my Angular application without encountering any console errors during runtime. It functions as expected; however, issues arise when running unit tests with Jasmine. To incorporate NgSelectMod ...

Issues with IE 11: SVG Map Not Triggering Mouseenter or Mouseleave Events

I've been grappling with this issue for the past couple of days and despite trying numerous solutions, nothing seems to be working. My SVG map of the US has jQuery mouseenter and mouseleave events that function properly in browsers other than IE 11 ( ...

Error: The dynamic selection function is experiencing an issue where it is unable to read the "map" property of an undefined

Currently, I am in the process of creating a React component that includes the usage of a select HTML input. The implementation is defined as shown below: <select className="form-control-mt-3" id="clientList" name="clientList" onChange={this.handleC ...

Guide on transforming the best.pt model of YOLOv8s into JavaScript

After successfully training a custom dataset on YOLOv8s model using Google Colab, I now have the best.pt file that I want to integrate into a web app via JavaScript. I've come across mentions of TensorFlow.js as a potential solution, but I'm stil ...

"Enhance Your Video Experience with a Custom

Currently, I am working on designing a front-page that features a YouTube video as the background with a fixed transparent navigation. Although I have successfully implemented both elements, I am struggling to make sure the video background starts at the t ...

Struggling to retrieve data from AJAX POST request [Revised Post]

I am encountering an issue with posting a form using the AJAX POST method in jQuery. I am using the serialize method to retrieve the form data, but it seems to fail. The problem might be related to the JavaScript files of the Steps Wizard plugin that I am ...

Importing the Monday view in React - A step-by-step guide

I am interested in incorporating one of my Monday boards into my React component. Here is a snippet of the code: import React from 'react' import useI18n from 'hooks/useI18n' import Page from 'components/layout/Page' const Ro ...

AngularJS - Setting an initial delay for ng-bind

We have a span element with the following attributes: <span role="link" ng-show="showLink()" ng-bind="textLink"></span> (Just an fyi: we implemented a fade-in, fade-out animation for this link, hence the use of ng-show instead of ng-if) The ...

Hover over two different divs with JQuery

I have a situation where I have two HTML table rows. When I hover over the first row, I want to display the second row. However, once the mouse leaves both rows, the second row should be hidden. Is there a way to achieve this using JQuery? <tr class=" ...

The width of Material UI Grid decreases every time it is re-rendered

I'm attempting to display a list of 25 words in a 5x5 grid using the MUI Grid component. The grid is structured as a <Grid container direction="column"> with five <Grid item> elements. Each <Grid item> contains: <Grid co ...

Why is the type of parameter 1 not an 'HTMLFormElement', causing the failure to construct 'FormData'?

When I try to execute the code, I encounter a JavaScript error. My objective is to store the data from the form. Error Message TypeError: Failed to create 'FormData': argument 1 is not an instance of 'HTMLFormElement'. The issue arise ...