I have my context Provider in my AppContext.tsx file
import { useState, useEffect, useContext, createContext } from 'react';
interface AppContextProps {
children: React.ReactNode
}
const themes = {
dark: {
backgroundColor: 'black',
color: 'white'
},
light: {
backgroundColor: 'white',
color: 'black'
}
}
const initialState = {
dark: false,
theme: themes.light,
toggle: () => { console.log('toggle from initalState') }
}
const AppContext = createContext(initialState);
export function AppWrapper({ children }: AppContextProps) {
const [dark, setDark] = useState(false) // Default theme is light
// On mount, read the preferred theme from the persistence
useEffect(() => {
const isDark = localStorage.getItem('dark') === 'true'
setDark(isDark)
}, [dark])
// To toggle between dark and light modes
const toggle = () => {
console.log('toggle from AppWrapper')
const isDark = !dark
localStorage.setItem('dark', JSON.stringify(isDark))
setDark(isDark)
}
const theme = dark ? themes.dark : themes.light
return (
<AppContext.Provider value={{ theme, dark, toggle }}>
{children}
</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
I insert Appwrapper into my _app.tsx file
import type { ReactElement, ReactNode } from 'react'
import type { NextPage } from 'next'
import type { AppProps } from 'next/app'
import { AppWrapper } from '../contexts/AppContext';
import 'normalize.css/normalize.css';
import '../styles/globals.scss'
type NextPageWithLayout = NextPage & {
getLayout?: (page: ReactElement) => ReactNode
}
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout
}
const App = ({ Component, pageProps }: AppPropsWithLayout) => {
const getLayout = Component.getLayout ?? ((page) => page)
return getLayout(
<AppWrapper>
<Component {...pageProps} />
</AppWrapper>
)
}
export default App
And I call and import the useAppContext hook to use it in my Header component
import styles from './header.module.scss'
import { useAppContext } from '../contexts/AppContext'
const Header = () => {
const { theme, toggle, dark } = useAppContext()
return (
<header className={styles.header}>
<nav className={styles.nav}>
<div onClick={toggle} className={styles.switchWrap}>
<label className={styles.switch}>
<input type="checkbox" />
<span className={styles.slider}></span>
</label>
</div>
</nav>
</header>
)
}
export default Header
However, my toggle function logs toggle from initalState instead of toggle from AppWrapper.
The following code below
const { theme, toggle, dark } = useAppContext()
gets data from initialState
const initialState = {
dark: false,
theme: themes.light,
toggle: () => { console.log('toggle from initalState') }
}
instead of what I pass into the value props shown below
<AppContext.Provider value={{ theme, dark, toggle }}>
{children}
</AppContext.Provider>
How do I correctly pass the value props data into my component instead of the data from initialState?
Because my app was using per-page layouts, I inserted AppWrapper in my layout file instead of _app.tsx and everything works
import Header from './header'
import Footer from './footer'
import { AppWrapper } from '../contexts/AppContext';
interface LayoutProps {
children: React.ReactNode
}
const Layout = ({ children }: LayoutProps) => {
return (
<AppWrapper>
<Header />
{children}
<Footer />
</AppWrapper>
)
}
export const getLayout = (page: LayoutProps) => <Layout>{page}</Layout>;
export default Layout
Related
I followed many solutions like this https://stackoverflow.com/a/71081567/19460332 and the official solution for this https://github.com/mui/material-ui/blob/master/examples/nextjs but nothing worked for me. I am using nextjs , material ui and js-cookies
the issue I am facing is for the MUI Switch
have a look to my code here https://github.com/MaheshYadavGitHub/pizza-gallery:
Here is the code for the section that is breaking :
below is the Layout.js where the Switch to toggle darkMode is not working as expected. The darkTheme gets applied as it gets from the cookies but the switch button still stays in off position on the first render but after another render it works perfectly and when again manually refreshed it does the same behaviour :-
import { useState, useContext } from "react";
import {
Container,
AppBar,
Toolbar,
Typography,
Link,
Switch,
CssBaseline,
ThemeProvider,
createTheme,
} from "#material-ui/core";
import { red } from "#material-ui/core/colors";
import Head from "next/head";
import Image from "next/image";
import NextLink from "next/link";
import useStyles from "../utils/styles";
import { Store } from "../utils/Store";
import Cookies from "js-cookie";
const Layout = ({ title, children }) => {
const { state, dispatch } = useContext(Store);
const { darkMode } = state;
const classes = useStyles();
const theme = createTheme({
palette: {
type: darkMode ? "dark" : "light",
primary: {
main: "#556cd6",
},
secondary: {
main: "#19857b",
},
error: {
main: red.A400,
},
},
typography: {
fontFamily: ["Arial", "Roboto Condensed"].join(","),
fontWeight: 700,
h1: {
fontSize: "1.6rem",
margin: "1rem 0rem",
fontWeight: 600,
"#media (min-width:600px)": {
fontSize: "2.6rem",
},
},
h6: {
fontWeight: 700,
},
navLinks: {
color: "#fff",
},
},
});
const handleThemeChange = (event) => {
dispatch({ type: darkMode ? "DARK_MODE_OFF" : "DARK_MODE_ON" });
const newThemeMode = !darkMode;
Cookies.set("darkMode", newThemeMode ? "ON" : "OFF");
};
return (
<>
<Head>
<title>{title ? `${title} - Pizza Gallery` : "Pizza Gallery"}</title>
</Head>
<ThemeProvider theme={theme}>
<CssBaseline />
<AppBar position="static" className={classes.navbar}>
<Toolbar>
<NextLink href="/" passHref>
<Link>
<Typography
variant="h6"
className={classes.brand}
component="div"
>
Pizza Gallery
</Typography>
</Link>
</NextLink>
<div className={classes.grow}></div>
<Switch
id="switch"
checked={darkMode}
onChange={handleThemeChange}
color="primary"
></Switch>
<NextLink href="/login" passHref>
<Link>
<Typography>Login</Typography>
</Link>
</NextLink>
<NextLink href="/cart" passHref>
<Link>
<Typography>Cart</Typography>
</Link>
</NextLink>
</Toolbar>
</AppBar>
<Container className={classes.main}>{children}</Container>
<footer className={classes.footer}>
<Typography>all rights reserved © pizza gallery 2022</Typography>
</footer>
</ThemeProvider>
</>
);
};
export default Layout;
below is the Store :-
import Cookies from "js-cookie";
import { createContext, useReducer } from "react";
export const Store = createContext();
const initialState = {
darkMode: Cookies.get("darkMode") === "ON" ? true : false,
};
const reducer = (state, action) => {
switch (action.type) {
case "DARK_MODE_ON":
return { ...state, darkMode: true };
case "DARK_MODE_OFF":
return { ...state, darkMode: false };
default:
return state;
}
};
export const StoreProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const value = { state, dispatch };
return <Store.Provider value={value}>{children}</Store.Provider>;
};
/pages/_app.js
import { useEffect } from "react";
import "../styles/globals.css";
import { StoreProvider } from "../utils/Store";
function MyApp({ Component, pageProps }) {
useEffect(() => {
const jssStyles = document.querySelector("#jss-server-side");
if (jssStyles) {
jssStyles.parentElement.removeChild(jssStyles);
}
}, []);
return (
<StoreProvider>
<Component {...pageProps} />
</StoreProvider>
);
}
export default MyApp;
/pages/_document.js
import { ServerStyleSheets } from "#material-ui/core/styles";
import Document, { Html, Head, Main, NextScript } from "next/document";
import React from "react";
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head></Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
MyDocument.getInitialProps = async (ctx) => {
const sheets = new ServerStyleSheets();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () => {
return originalRenderPage({
enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
});
};
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: [
...React.Children.toArray(initialProps.styles),
sheets.getStyleElement(),
],
};
};
If you're not using Server Side Rendering for that component, you may try importing the component, by turning off ssr:
import dynamic from 'next/dynamic';
const Component = dynamic(() =>
import('../components/path_to_component'), {
ssr: false,
});
Hope this helps!
I'm having some problems to set a state when repass the context provider for other elements, this is my code.
I'm creating a FancyboxContext for when i need it, i can call it anywhere.
import React, { createContext, useContext, useState } from 'react';
interface FancyboxContextInterface {
fancybox: boolean;
setFancybox(value: boolean): void;
}
interface FancyboxProviderProps {
children: React.ReactNode;
}
interface UseFancyboxInterface {
fancybox: boolean;
setFancybox: React.Dispatch<React.SetStateAction<boolean>>;
}
const FancyboxInitialState = {
fancybox: false,
setFancybox: () => {}
};
const FancyboxContext = createContext<FancyboxContextInterface>(
FancyboxInitialState
);
const FancyboxProvider: React.FC<FancyboxProviderProps> = ({
children
}: FancyboxProviderProps) => {
const [fancybox, setFancybox] = useState(FancyboxInitialState.fancybox);
return (
<FancyboxContext.Provider
value={{
fancybox,
setFancybox
}}
>
{children}
</FancyboxContext.Provider>
);
};
export const useFancybox = (): UseFancyboxInterface => {
const context = useContext(FancyboxContext);
const { fancybox, setFancybox } = context;
return {
fancybox,
setFancybox
};
};
export default FancyboxProvider;
involving _document.tsx in ContextProvider;
...
render(): JSX.Element {
return (
<Html lang="pt">
<Head>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Roboto:wght#300;400;700&display=swap"
/>
</Head>
<body>
<FancyboxProvider>
<Main />
</FancyboxProvider>
<NextScript />
</body>
</Html>
);
}
but when I am trying to start a simple call to setState(), nothing happens
import React, { useEffect } from 'react';
import { AppProps } from 'next/app';
import { useFancybox } from '../hooks/useFancybox';
import { Fancybox, GlobalStyles } from '../styles/global';
const myApp: React.FC<AppProps> = ({ Component, pageProps }) => {
const { fancybox, setFancybox } = useFancybox();
useEffect(() => setFancybox(true), []);
return (
<>
{fancybox ? <Fancybox /> : ''}
<Component {...pageProps} />
<GlobalStyles />
</>
);
};
export default myApp;
First, you should know that React Context can only be used on the client side.
You cannot place your ContextProvider in _document.js because it only runs on the server side. in your _app.js you are exporting a javascript function myApp instead of a React component MyApp.
What you can do is place your FancyProvider in _app.js and call your setState in any page
what your _app.js should looks like
import React from "react";
import { AppProps } from "next/app";
import { GlobalStyles } from "../styles/global";
const MyApp: React.FC<AppProps> = ({ Component, pageProps }) => {
return (
<FancyboxProvider>
<Component {...pageProps} />
<GlobalStyles />
</FancyboxProvider>
);
};
export default MyApp;
Now you can call your setState on /pages/index.js or any other page
import { useEffect } from "react";
import { useFancybox } from "../hooks/useFancybox";
import { Fancybox } from "../styles/global";
export default function Home() {
const { fancybox, setFancybox } = useFancybox();
useEffect(() => {
setFancybox(true);
}, []);
return (
<div>
{fancybox ? <Fancybox /> : ""}
</div>
);
}
I'm using the Context API to store the theme value. The theme itself is created with <createMuiTheme> and passed down from <Layout> to children via <MuiThemeProvider> and <CssBaseline>. I can see the state change via React DevTools but the theme itself is not being applied - and I'm at loss as to why...
Here is codesandbox with a full example - warning: contains Samuel L. Ipsum. What follows is an abridged version.
Default and dark theme definitions:
// theme/dark.js
import { createMuiTheme } from "#material-ui/core/styles";
const theme = createMuiTheme({
typography: {
useNextVariants: true
},
palette: {
type: "dark"
}
});
export default theme;
// theme/default.js
import { createMuiTheme } from "#material-ui/core/styles";
const theme = createMuiTheme({
typography: {
useNextVariants: true
},
palette: {
type: "light"
}
});
export default theme;
Context:
// context/settings/SettingsContext.js
import React from "react";
export default React.createContext({
darkMode: false
});
// context/settings/SettingsProvider.js
import React, { useState } from "react";
import SettingsContext from "./SettingsContext";
const storage = {
getItem(key) {
if (localStorage) {
return localStorage.getItem(key);
}
},
setItem(key, value) {
if (localStorage) {
return localStorage.setItem(key, value);
}
}
};
const SettingsProvider = props => {
const [darkMode, setDarkMode] = useState(
storage.getItem("darkMode") === "true"
);
const onSetDarkMode = darkMode => {
setDarkMode(darkMode);
storage.setItem("darkMode", darkMode);
};
return (
<SettingsContext.Provider
value={{
darkMode,
onSetDarkMode
}}
>
{props.children}
</SettingsContext.Provider>
);
};
export default SettingsProvider;
index.js:
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import App from "./containers/app";
import SettingsProvider from "./context/settings/SettingsProvider";
ReactDOM.render(
<BrowserRouter>
<SettingsProvider>
<App />
</SettingsProvider>
</BrowserRouter>,
document.getElementById("root")
app/index.js:
import React, { useState } from "react";
import { Switch, Route } from "react-router-dom";
import { default as home } from "../home/routes";
import Layout from "../layout";
const App = () => {
const [anchorEl, setAnchorEl] = useState(null);
return (
<div>
<Layout anchorEl={anchorEl} setAnchorEl={setAnchorEl}>
<Switch>
{home.map((route, index) => (
<Route
key={index}
path={route.path}
exact={route.exact}
render={route.render}
/>
))}
</Switch>
</Layout>
</div>
);
};
export default App;
And layout/index.js:
import React, { useContext } from "react";
import { MuiThemeProvider } from "#material-ui/core/styles";
import { makeStyles } from "#material-ui/core/styles";
import CssBaseline from "#material-ui/core/CssBaseline";
import defaultTheme from "../../themes/default";
import darkTheme from "../../themes/default";
import SettingsContext from "../../context/settings/SettingsContext";
import Header from "../../components/header/index";
const useStyles = makeStyles(theme => ({
toolbarMargin: {
...theme.mixins.toolbar
}
}));
const Layout = props => {
const classes = useStyles();
const context = useContext(SettingsContext);
const theme = context.darkMode ? darkTheme : defaultTheme;
const { children, anchorEl, setAnchorEl } = props;
return (
<MuiThemeProvider theme={theme}>
<CssBaseline />
<Header anchorEl={anchorEl} setAnchorEl={setAnchorEl} />
<main>
<div className={classes.toolbarMargin} />
{children}
</main>
</MuiThemeProvider>
);
};
export default Layout;
What did I miss?
You're importing the same theme twice. I'd suggest using named exports instead of defaults, it helps a lot with auto importing, as well as spotting mistakes like this.
// layout/index.js
import defaultTheme from "../../themes/default";
import darkTheme from "../../themes/default"; // should be "../../theme/dark"
can you please help me to change the React Material UI theme Dynamically .
https://imgur.com/S8zsRPQ
https://imgur.com/Ul8J40N
I have tried by changing the theme Properties on button click . The theme properties are getting changed as seen in the console . But the change is not reflecting on the theme .
Sandbox Code : https://codesandbox.io/s/30qwyk92kq
const themeChange = () => {
alert(theme);
theme.palette.type = "light";
console.log(theme);
};
ReactDOM.render(
<MuiThemeProvider theme={theme}>
<React.Fragment>
<CssBaseline />
<App changeTheme={themeChange} />
</React.Fragment>
</MuiThemeProvider>,
document.getElementById("app")
);
When I click the button the theme has to change to Dark color
I am using styledComponents, typescript and material-ui.
First I defined my themes:
// This is my dark theme: dark.ts
// I defined a light.ts too
import createMuiTheme from '#material-ui/core/styles/createMuiTheme';
export const darkTheme = createMuiTheme({
palette: {
type: 'dark', // Name of the theme
primary: {
main: '#152B38',
},
secondary: {
main: '#65C5C7',
},
contrastThreshold: 3,
tonalOffset: 0.2,
},
});
I defiend a themeProvider function and in this function I wrapped the material-ui's ThemeProvider in a React context to be able to change the theme easily:
import React, { useState } from 'react';
import {ThemeProvider} from "#material-ui/core/styles/";
import { lightTheme } from "./light";
import { darkTheme } from "./dark";
const getThemeByName = (theme: string) => {
return themeMap[theme];
}
const themeMap: { [key: string]: any } = {
lightTheme,
darkTheme
};
export const ThemeContext = React.createContext(getThemeByName('darkTheme'));
const ThemeProvider1: React.FC = (props) => {
// State to hold the selected theme name
const [themeName, _setThemeName] = useState('darkTheme');
// Retrieve the theme object by theme name
const theme = getThemeByName(themeName);
return (
<ThemeContext.Provider value={_setThemeName}>
<ThemeProvider theme={theme}>{props.children}</ThemeProvider>
</ThemeContext.Provider>
);
}
export default ThemeProvider1;
Now I can use it in my components like this:
import React from 'react';
import styled from 'styled-components';
import useTheme from "#material-ui/core/styles/useTheme";
const MyCardHeader = styled.div`
width: 100%;
height: 40px;
background-color: ${props => props.theme.bgColor};
color: ${props => props.theme.txtColor};
display: flex;
align-items:center;
justify-content: center;
`;
export default function CardHeader(props: { title: React.ReactNode; }) {
const theme = {
bgColor: useTheme().palette.primary.main,
txtColor: useTheme().palette.primary.contrastText
};
return (
<MyCardHeader theme={theme}>
{props.title}
</MyCardHeader>
);
}
For Changing between themes:
import React, {useContext} from 'react';
import { ThemeContext} from './themes/themeProvider';
export default function Header() {
// Get the setter function from context
const setThemeName = useContext(ThemeContext);
return (
<header>
<button onClick={() => setThemeName('lightTheme')}>
light
</button>
<button onClick={() => setThemeName('darkTheme')}>
dark
</button>
</header>
);
}
I'm using Material UI v4.
I tried something like Ashkan's answer, but it didn't work for me.
However, I found this in the documentation, and abstracting it to apply to a different piece of state, instead of user preference, worked for me. For your example, I'd probably make a context:
// context.js
import React, { useContext } from "react";
import { ThemeProvider, createTheme } from "#material-ui/core/styles";
import CssBaseline from "#material-ui/core/CssBaseline";
const CustomThemeContext = React.createContext();
// You can add more to these and move them to a separate file if you want.
const darkTheme = {
palette: {
type: "dark",
}
}
const lightTheme = {
palette: {
type: "light",
}
}
export function CustomThemeProvider({ children }) {
const [dark, setDark] = React.useState(false);
function toggleTheme() {
if (dark === true) {
setDark(false);
} else {
setDark(true);
}
}
const theme = React.useMemo(
() => {
if (dark === true) {
return createTheme(darkTheme);
}
return createTheme(lightTheme);
},
[dark],
);
return (
<CustomThemeContext.Provider value={toggleTheme}>
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
</CustomThemeContext.Provider>
);
}
export function useToggleTheme() {
const context = useContext(CustomThemeContext);
if (context === undefined) {
throw new Error("useCustomThemeContext must be used within an CustomThemeProvider");
}
return context;
}
Then wrap your app in that:
ReactDOM.render(
<CustomThemeProvider>
<App />
</CustomThemeProvider>,
document.getElementById("app")
);
And then access it in your app:
export default function App(){
const toggleTheme = useToggleTheme();
return (
<div>
<button onClick={toggleTheme}>Toggle the theme!!</button>
</div>
);
}
On a side note, in my app, I actually have a different theme in two sections of the app, based on whether the user is logged in or not. So I'm just doing this:
function App() {
const { authState } = useAuthContext();
const theme = React.useMemo(
() => {
if (authState.user) {
return createTheme(dashboardTheme);
}
return createTheme(loginTheme);
},
[authState.user],
);
return (
<ThemeProvider theme={theme}>
<TheRestOfTheApp />
</ThemeProvider>
}
It seems you can base the theme off any piece of state, or multiple pieces, by referencing them in useMemo and including them in the dependency array.
EDIT:
I just noticed that MUI v5 actually has something very similar in their docs.
In your code, theme type is changed. But the Page is not re-rendered with new theme.
I have changed code in index.js and App.js like following.
Try this approach. It works.
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(
<App/>,
document.getElementById("app")
);
App.js
import React from "react";
import CssBaseline from "#material-ui/core/CssBaseline";
import Typography from "#material-ui/core/Typography";
import { Button } from "#material-ui/core";
import { MuiThemeProvider, createMuiTheme } from "#material-ui/core/styles";
import blueGrey from "#material-ui/core/colors/blueGrey";
import lightGreen from "#material-ui/core/colors/lightGreen";
class App extends React.Component {
constructor(props){
super(props);
this.state = {
themeType : 'dark',
}
}
changeTheme(){
if (this.state.themeType == 'dark'){
this.setState({themeType:'light'});
} else {
this.setState({themeType:'dark'});
}
}
render() {
let theme = createMuiTheme({
palette: {
primary: {
light: lightGreen[300],
main: lightGreen[500],
dark: lightGreen[700]
},
secondary: {
light: blueGrey[300],
main: blueGrey[500],
dark: blueGrey[700]
},
type: this.state.themeType
}
});
return (
<MuiThemeProvider theme={theme}>
<CssBaseline />
<Typography>Hi there!</Typography>
<Button
variant="contained"
color="secondary"
onClick={()=>{this.changeTheme()}}
>
Change
</Button>
</MuiThemeProvider>
);
}
}
export default App;
I try to create a Theme for all of my components in the app, I tried to but failed with /pages/_app.js:
import App, { Container } from "next/app"; // eslint-disable-line
import React from "react";
import { ThemeProvider } from "styled-components";
export default class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps };
}
render() {
const { Component, pageProps } = this.props;
return (
<Container>
<ThemeProvider theme={{ color: 'blue' }}>
<Component {...pageProps} />
</ThemeProvider>
</Container>
);
}
}
But in my components theme prop is always an empty Object:
const A = styled.a`
color: ${props => {
console.log(props);
return "blue";
}};
`;
It should be
const A = styled.a`
color: ${props => props.theme.color};
`;