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

Incorporating traditional Javascript classes for modeling in React development

Can traditional JavaScript classes be utilized in models within the MVC framework while using React, as opposed to relying on Redux or contexts & reducers which may impact reusability? If this approach is feasible, how can we efficiently 'subscribe&ap ...

Exploring PHP cURL with the power of jQuery

function php_download($Url){ $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $Url); curl_setopt($ch, CURLOPT_REFERER, "http://www.example.org/yay.htm"); curl_setopt($ch, CURLOPT_USERAGENT, "MozillaXYZ/1.0"); curl_setopt($ch, CURLOPT_H ...

leveraging a callback function alongside the useState hook

I'm facing an issue with the change() function. My goal is to call the filteredData() function after the setState operation is completed. Typically, I would use a callback function for this task, but useState doesn't support callbacks. Using useE ...

Error message: "SyntaxError: Unexpected token import was not caught by the foundation"

I have recently taken over development from a previous developer who integrated Zurb Foundation as a framework into our website. The Foundation framework was installed using npm. I am encountering errors in the console related to all of the foundation java ...

Is it possible to dynamically adjust the size of the CircleProgressComponent element in ng-circle-progress?

For my current Angular 11 project, I am facing the challenge of dynamically changing the size of the ng-circle-progress library's CircleProgressComponent element. After some research, I discovered that the element's size can be adjusted by apply ...

issues related to implementing search functionality with react-redux

Just starting out with my first react-redux project which is a list of courses, but I have hit a roadblock with redux. I am trying to implement a search functionality based on this answer, and while I can see the action in redux-devtools, it's not ref ...

Unexpected appearance of a blue line in Material UI when the overflow attribute is included

For my React application, I've integrated Material-UI along with styled components. The strange thing is that when I use a Chrome browser to view the app, I encounter an issue that doesn't seem to happen in Firefox. The problem arises when I add ...

What are the steps to transform my database object into the Material UI Table structure?

I have a MongoDB data array of objects stored in products. The material design format for creating data rows is as follows: const rows = [ createData('Rice', 305, 3.7, 67, 4.3), createData('Beans', 452, 25.0, 51, 4.9), createData ...

Switch the orientation of a live table moving horizontally to vertically and vice versa

config.previewData = [ { Products:27989, Total Customers:294, Metrics:"MVC", Toner Products:5928, INK Products:22061 }, { Products:56511, Total Customers:376, Metrics:"SMB", ...

I am in need of a JavaScript event that will take precedence over the CSS pointer

There is an image causing issues with scrolling on a mobile device because it's located in the finger-friendly area where people tend to scroll. I attempted to use pointer-events: null; to allow scrolling, but this also prevented the click event from ...

Create a regular expression that permits a sequence of numbers (either integer or decimal) arranged in groups of up to five, with each

Is it possible to create a regular expression that allows integers and decimals? var reg = /^((\s*)|([0-9]\d{0,9}(\.\d{1,3})?%?$))$/.; How can users input 0 to 5 groups of integers and decimals separated by |? Updated: This regex sh ...

changing the vertical position of an element using JavaScript

I'm currently working on a project where I have a canvas that animates upwards when a button is clicked. However, I'm facing an issue with making it go back down after the animation completes. It seems to be getting stuck and not returning to its ...

Difficulty with Bootstrap 4 mobile navbar dropdown feature

<div class="baslik baslik1 baslik2 "> <nav class="navbar bg-light navbar-light navbar-expand-sm sticky-top "> <a href="./index.html" class="navbar-brand"><img src="img/512x512logo.png" ...

The process involves transferring information from a specific div element to a database. This particular div element obtains its data after receiving an appended ID

It's a bit complicated, but I'm working on creating a tag system. I'm fetching data from a database with an AJAX call using the "@" symbol. Everything is working fine - the tags are being generated, but I'm having trouble retrieving and ...

The bundle.js file encountered an issue while running UglifyJs, expecting a name

I have been attempting to utilize UglifyJS to compress/minimize my bundle.js file. However, when executing webpack -p, I encountered the following error message: ERROR in bundle.js from UglifyJs Name expected [bundle.js:105519,6] The line causing the iss ...

In Next.js, the elements inside the div created by glider-js are not properly loaded

I'm currently working on setting up a carousel in nextjs using the data retrieved from an API and utilizing glider-js for this purpose. However, I'm facing an issue where the div created by glinder-js does not include the elements that are render ...

Is there a simple method to submit to a URL without relying on ajax?

When it comes to using jQuery, the $.ajax() function is typically used for POST requests to a URL. However, in my particular situation, I am unable to use this function. I need the client to send a POST request to a URL and have the server redirect the use ...

Dropdown element with PrimeNG adorned with a span

I am trying to create a simple form with text inputs and dropdowns. I have successfully implemented the textInput, but I am struggling with the dropdowns. Below is the code that I have so far: <div class="p-grid p-dir-col p-offset-2"> ...

The Vue 3 router seems to be malfunctioning and I am quite puzzled as to why

As someone new to Vue (specifically Vue 3), I decided to test a mock Vue application. After successfully testing the default homepage, I wanted to explore creating multiple pages. Despite following tutorials step by step, I've encountered an issue whe ...

Conversion of UTC timestamp to a timestamp in the specified timezone

this.selectedTimezone="Pacific/Kiritimati"; //this value will come from a dropdown menu These records represent the data.body returned by an API call. Iterating through each record in the dataset: { We are creating a new Date object based on the ...