How can I update Material UI theme dynamically via Redux store?

Viewed 4426

I have the following React component that uses a custom Material UI theme

const getTheme = name => themes.filter(theme => theme.name === name)[0] || themes[0];

const Root = props => (
    <MuiThemeProvider muiTheme={getMuiTheme(getTheme(props.theme).data)}>
        <Router history={history}>
            <Switch>
                <Route component={AppContainer}/>
            </Switch>
        </Router>
    </MuiThemeProvider>
);

It's fed by a container

const mapStateToProps = state => ({
    theme: state.settings.theme
});

const RootContainer = connect(mapStateToProps, {})(Root);

When I update the theme name on a settings page the state is updated (confirmed via Redux logging), but the theme isn't updated. However, when I navigate away from the page, the new theme is applied.

Presumably the change in state isn't causing the Root component to re-render, or there's something I don't understand in the way this is applied

<MuiThemeProvider muiTheme={getMuiTheme(getTheme(props.theme).data)}>

Any idea how I can get the theme to update the instance it's changed in the state?

2 Answers

Turns out I had to also pass the updated theme as a prop to my App component (just below Root in the hierarchy). Seems like it's to do with the fact that the theme is passed via the context and changing a prop in the component that configures the theme wasn't enough to trigger the required updates.

Found a solution for this. While theme is provided to ReactJs threw ReactJs Context API it, check Caveats section of ReactJs docs: https://reactjs.org/docs/context.html#caveats So we exactly need to create new object to re-render the context for consumers.

enter image description here

So, what i did is this:

  1. I created custom hook to get if mode should be light or dark:

import { useEffect } from "react";
import { useState } from "react";

export default function useGetThemeMode() {
  const initialMode =
    window.matchMedia &&
    window.matchMedia("(prefers-color-scheme: dark)").matches === true
      ? "dark"
      : "light";

  const [themeMode, setThemeMode] = useState(initialMode);

  useEffect(() => {
    const interval = setInterval(() => {
      const newMode =
        window.matchMedia &&
        window.matchMedia("(prefers-color-scheme: dark)").matches === true
          ? "dark"
          : "light";
      if (themeMode !== newMode) {
        setThemeMode(newMode);
      }
    }, 5000);

    return () => clearInterval(interval);
  }, [themeMode]);

  return themeMode;
}

And then in Root component i provide this theme object to ThemeProvider:

  const themeMode = useGetThemeMode();
  const theme = createTheme(getCustomThemeStyles());
  theme.palette.mode = themeMode; // <---- The key line
Related