I am using mui5(Material Ui) in my nextjs application. I am trying to implementing dark mode. All are going well. I want a feature that if any user toggle the dark mode then it will be saved in local-storage. Then when I refresh the page, It automatically getting value from local-storage and active dark or light mode according to value from local-storage. If user first come to the site then it should active automatically system preference mode. I mean If there are no value in the local-storage then it should active automatically system preference mode. How can I do that.
Here is my code-
_app.js
export default function MyApp(props) {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
const [mode, setMode] = React.useState("light");
const colorMode = React.useMemo(
() => ({
// The dark mode switch would invoke this method
toggleColorMode: () => {
setMode((prevMode) =>
prevMode === 'light' ? 'dark' : 'light',
);
},
}),
[],
);
// Update the theme only if the mode changes
const muiTheme = React.useMemo(() => createTheme(theme(mode)), [mode]);
return (
<ColorModeContext.Provider value={colorMode}>
<CacheProvider value={emotionCache}>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<ThemeProvider theme={muiTheme}>
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
</CacheProvider>
</ColorModeContext.Provider>
);
}
MyApp.propTypes = {
Component: PropTypes.elementType.isRequired,
emotionCache: PropTypes.object,
pageProps: PropTypes.object.isRequired,
};
Toogle button-
const theme = useTheme();
const colorMode = useContext(ColorModeContext);
<FormControlLabel
control={<MaterialUISwitch sx={{ m: 1 }} checked={theme.palette.mode === 'dark' ? false : true} />}
label=""
sx={{ mx: "0px" }}
onClick={colorMode.toggleColorMode}
/>
Here you can have an example of what I've done with Next.js and Material UI (5) to:
Have 2 themes available: lightTheme and darkTheme.
Have a ThemeSwitcherButton component so we can swtich between both themes.
Create a new ThemeProvider and ThemeContext to store the selected theme mode value, provide access to read and change it.
Store the preference of the user on Local Storage using a useLocalStorage hook.
Load the theme mode reading from the browser preference if there's no storage value, using the Material UI useMediaQuery hook.
I'm using Typescript, but it doesn't matter if you use plain JavaScript
Create the 2 themes needed:
We'll have 2 files to modify the specific properties independently.
darkTheme.ts
import { createTheme } from '#mui/material/styles'
const darkTheme = createTheme({
palette: {
mode: 'dark',
},
})
export default darkTheme
lightTheme.ts
import { createTheme } from '#mui/material/styles'
const lightTheme = createTheme({
palette: {
mode: 'light',
},
})
export default lightTheme
Create the switcher button:
The important thing here is the value and function retrieved from the context, the button style or icon can be anything.
interface ThemeSwitcherButtonProps extends IconButtonProps { }
const ThemeSwitcherButton = ({ ...rest }: ThemeSwitcherButtonProps) => {
const { themeMode, toggleTheme } = useThemeContext()
return (
<Tooltip
title={themeMode === 'light' ? `Switch to dark mode` : `Switch to light mode`}
>
<IconButton
{...rest}
onClick={toggleTheme}
>
{themeMode === 'light' ? <DarkModeOutlined /> : <LightModeRounded />}
</IconButton>
</Tooltip>
)
}
export default ThemeSwitcherButton
Create the ThemeContext, ThemeProvider, and useThemeContext:
We use useMediaQuery from material ui library to check the preference mode of the browser. This hook works with client rendering and ssr.
We also use useLocalStorage hook to save the state in the local storage so it's persisted.
We wrap the original Material UI ThemeProvider (renamed as MuiThemeProvider) with this new Provider, so then in the _app file only one Provider is needed
ThemeContext.tsx
import { createContext, ReactNode, useContext } from 'react'
import { ThemeProvider as MuiThemeProvider, useMediaQuery } from '#mui/material'
import lightTheme from '#/themes/light'
import darkTheme from '#/themes/dark'
import useLocalStorage from '#/react/hooks/useLocalStorage'
const DARK_SCHEME_QUERY = '(prefers-color-scheme: dark)'
type ThemeMode = 'light' | 'dark'
interface ThemeContextType {
themeMode: ThemeMode
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType>({} as ThemeContextType)
const useThemeContext = () => useContext(ThemeContext)
const ThemeProvider = ({ children }: { children: ReactNode }) => {
const isDarkOS = useMediaQuery(DARK_SCHEME_QUERY)
const [themeMode, setThemeMode] = useLocalStorage<ThemeMode>('themeMode', isDarkOS ? 'light' : 'dark')
const toggleTheme = () => {
switch (themeMode) {
case 'light':
setThemeMode('dark')
break
case 'dark':
setThemeMode('light')
break
default:
}
}
return (
<ThemeContext.Provider value={{ themeMode, toggleTheme }}>
<MuiThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
{children}
</MuiThemeProvider>
</ThemeContext.Provider>
)
}
export {
useThemeContext,
ThemeProvider
}
_app.tsx
export default function MyApp(props: MyAppProps) {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props
const getLayout = Component.getLayout ?? ((page) => page)
return (
<CacheProvider value={emotionCache}>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<ThemeProvider>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
{getLayout(<Component {...pageProps} />)}
</ThemeProvider>
</CacheProvider>
)
}
Create useLocalStorage hook:
I took the source from here, and modified it a little bit so could work properly with Next.js due to ssr and client rendering mismatches.
The useLocalStorage also uses another useEventListener hook, to synchronize the changes on the value among all other tabs opened.
useLocalStorage.tsx
// edited from source: https://usehooks-ts.com/react-hook/use-local-storage
// to support ssr in Next.js
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
import useEventListener from '#/react/hooks/useEventListener'
type SetValue<T> = Dispatch<SetStateAction<T>>
function useLocalStorage<T>(key: string, initialValue: T): [T, SetValue<T>] {
// Read local storage the parse stored json or return initialValue
const readStorage = (): T => {
if (typeof window === 'undefined') {
return initialValue
}
try {
const item = window.localStorage.getItem(key)
return item ? (parseJSON(item) as T) : initialValue
} catch (error) {
console.warn(`Error reading localStorage key “${key}”:`, error)
return initialValue
}
}
// Persists the new value to localStorage.
const setStorage: SetValue<T> = value => {
if (typeof window == 'undefined') {
console.warn(
`Tried setting localStorage key “${key}” even though environment is not a client`,
)
}
try {
// Allow value to be a function so we have the same API as useState
const newValue = value instanceof Function ? value(state) : value
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(newValue))
// We dispatch a custom event so every useLocalStorage hook are notified
window.dispatchEvent(new Event('local-storage'))
} catch (error) {
console.warn(`Error setting localStorage key “${key}”:`, error)
}
}
// State to store the value
const [state, setState] = useState<T>(initialValue)
// Once the component is mounted, read from localStorage and update state.
useEffect(() => {
setState(readStorage())
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
setStorage(state)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state])
const handleStorageChange = () => {
setState(readStorage())
}
// this only works for other documents, not the current one
useEventListener('storage', handleStorageChange)
// this is a custom event, triggered in writeValueToLocalStorage
// See: useLocalStorage()
useEventListener('local-storage', handleStorageChange)
return [state, setState]
}
export default useLocalStorage
// A wrapper for "JSON.parse()"" to support "undefined" value
function parseJSON<T>(value: string | null): T | undefined {
try {
return value === 'undefined' ? undefined : JSON.parse(value ?? '')
} catch (error) {
console.log('parsing error on', { value })
return undefined
}
}
The useEventListener.tsx is exactly the same as is in the web.
The code
All of the code of this example can be found in my github repository that can be used as a starting point for a project with Typescript, Nextjs and Material UI.
You can also try a working example here.
toggleColorMode: () => {
//use this line for save in localStorage
localStorage.setItem("mode",mode=== 'light' ? 'dark' : 'light' )
setMode((prevMode) =>
prevMode === 'light' ? 'dark' : 'light',
);
},
then write a useEffect to set mode based on localStorage
useEffect(()=>{
if( localStorage.getItem("mode")){
setMode(localStorage.getItem("mode"))
}
},[])
I followed the answer of #giorgiline and everything was fine till the local storage part, which for me seems to be a bit too complicated.
What I did instead is in the ThemeContext.tsx looks like this:
This bit is different:
const [themeMode, setThemeMode] = useState("light");
useEffect(() => {
// reading the storage
const stored = localStorage.getItem("theme");
setThemeMode(stored ? JSON.parse(stored) : "light");
}, []);
function updateTheme(theme: string){
// setting the storage
setThemeMode(theme)
localStorage.setItem("theme", JSON.stringify(theme));
}
const toggleTheme = () => {
switch (themeMode) {
case 'light':
updateTheme("highContrast")
break
case 'highContrast':
updateTheme("light")
break
default:
}
}
Here the whole file
import {createContext, ReactNode, useContext, useEffect, useState} from 'react'
import {createTheme, ThemeProvider as MuiThemeProvider} from '#mui/material'
import lightTheme from "../styles/theme/lightTheme";
import hightContrastTheme from "../styles/theme/hightContrastTheme";
import theme from "#storybook/addon-interactions/dist/ts3.9/theme";
interface ThemeContextType {
themeMode: string
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType>({} as ThemeContextType)
const useThemeContext = () => useContext(ThemeContext)
const ThemeProvider = ({ children }: { children: ReactNode }) => {
const [themeMode, setThemeMode] = useState("light");
useEffect(() => {
const stored = localStorage.getItem("theme");
setThemeMode(stored ? JSON.parse(stored) : "light");
}, []);
function updateTheme(theme: string){
setThemeMode(theme)
localStorage.setItem("theme", JSON.stringify(theme));
}
const toggleTheme = () => {
switch (themeMode) {
case 'light':
updateTheme("highContrast")
break
case 'highContrast':
updateTheme("light")
break
default:
}
}
return (
<ThemeContext.Provider value={{ themeMode, toggleTheme }}>
<MuiThemeProvider theme={themeMode === 'light' ? createTheme(lightTheme) : createTheme(hightContrastTheme)}>
{children}
</MuiThemeProvider>
</ThemeContext.Provider>
)
}
export {
useThemeContext,
ThemeProvider
}
Related
I have made a high contrast mode but I have gotten stuck with trying to save the new value to local storage, any guidance I will be appreciated.
Here is the themes wrapper where I am setting up the theme
import React, { useState, useEffect } from 'react';
import { ThemeContext, themes } from '../context/themeContext';
export function isOdd(num) {
console.log("isOdd",num % 2);
}
export default function ThemeContextWrapper(props) {
const [theme, setTheme] = useState(themes.dark);
function changeTheme(theme) {
setTheme(theme);
}
useEffect(() => {
switch (theme) {
case themes.light:
document.body.classList.add('white-content');
// Store
// Retrieve
break;
case themes.dark:
default:
document.body.classList.remove('white-content');
localStorage.setItem(theme, themes.dark);
localStorage.getItem(theme);
break;
}
}, [theme]);
return (
<ThemeContext.Provider value={{ theme: theme, changeTheme: changeTheme }}>
{props.children}
</ThemeContext.Provider>
);
}
and here is the on click listener that triggers the high contrast mode
import React, { useState, useEffect } from "react";
import 'devextreme/dist/css/dx.common.css';
import 'devextreme/dist/css/dx.material.blue.light.css';
// Custom icons by Ionicons
import 'ionicons/dist/css/ionicons.css';
import DarkModeicon from '../images/icons/dark-mode-icon.png'
import SpeedDialAction from 'devextreme-react/speed-dial-action';
import { isOdd } from '../themewrapper/themeWrapperContext';
import Sun from '../images/icons/sun.png'
import config from 'devextreme/core/config';
import notify from 'devextreme/ui/notify';
import '../App.css';
class FloatingActionButton extends React.Component {
constructor(props) {
super(props);
config({
floatingActionButtonConfig: {
icon: 'runner',
closeIcon: 'icon ion-md-close',
position: {
my: 'right bottom',
at: 'right bottom',
offset: '-16 -16'
}
}
});
}
// function enabled night mode
toggleTheme() {
isOdd(3)
console.log("toggleTheme click func")
document.body.classList.add('dark-content');
localStorage.setItem(theme, themes.dark);
localStorage.getItem(theme, themes.dark);
}
toggleDay() {
isOdd(3)
console.log("toggle day click func")
document.body.classList.remove('dark-content');
document.body.classList.add('white-content');
}
render() {
return (
<div id="app-container">
<SpeedDialAction
hint="Turn on Day Mode"
icon={Sun}
onClick={() => this.toggleDay()}
/>
<SpeedDialAction
hint="Increase Font"
icon="growfont"
onClick={() =>
alert("Increase Font Clicked!!")
}
/>
<SpeedDialAction
hint="turn on night mode"
icon={DarkModeicon}
onClick={() => this.toggleTheme()}
/>
</div>
);
}
}
export default FloatingActionButton;
I tried to set up the local storage in both the on click listener and theme wrapper no success.
As using the Context.Provider' to provide the value to the child, you should also use Context.Consumer' in the child which needs to pick the value from provider.
So in FloatingActionButton component, you might use this instead:
class FloatingActionButton extends React.Component {
// ...some other code
toggleTheme(theme, changeTheme) {
let newTheme;
if (theme === themes.light) {
newTheme = themes.dark;
} else {
newTheme = themes.light;
}
changeTheme(newTheme); // set the theme immediately
localStorage.setItem('theme', newTheme); // then save it into localStorage
}
render() {
return (
<ThemeContext.Consumer>
{(theme, changeTheme) => (
// ...some other elements
<button onClick={() => toggleTheme(theme, changeTheme)}>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
)
}
}
Then you can detect the theme in the ThemeContextWrapper component:
export default function ThemeContextWrapper(props) {
const [theme, setTheme] = useState(); // keep the default value as null because it will get the value in localStorage when useEffect detect the first render.
function changeTheme(theme) {
setTheme(theme);
}
useEffect(() => {
switch (theme) {
case themes.light:
document.body.classList.add('white-content');
break;
case themes.dark:
document.body.classList.remove('white-content');
default:
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
setTheme(savedTheme)
} else { // if there is no theme data in localStorage, set theme to dark mode
setTheme(themes.dark)
}
break;
}
}, [theme]);
return (
<ThemeContext.Provider value={{ theme: theme, changeTheme: changeTheme }}>
{props.children}
</ThemeContext.Provider>
);
}
What I try to do
I try to use apiContext with ThemeProvider, when the user want switch a light mode to dark mode, my state doesn't want change.
My operative mode
I create an interface themeContext files for manage types and initialize context.
A themeContext file for call createContext and made a custom hook.
A ThemeProvider for pass my constant 'theme' is always a boolean and an a method for switch it.
In my template I have a part of logic for call the hook and mutate turn the theme.
My console return always false, what's going wrong. I deliberately omitted react's imports.
My code
IthemeContext.ts
export interface IThemeContext {
theme: boolean;
toggleTurn?: () => void;
}
export const defaultThemeState: IThemeContext = {
theme: false,
toggleTurn: () => !defaultThemeState.theme
}
themeContext.ts
import {defaultThemeState, IThemeContext} from "../interface/IThemeContext";
const ThemeContext = React?.createContext<Partial<IThemeContext>>(defaultThemeState);
export const useThemeContext = () => useContext(ThemeContext);
export default ThemeContext;
ThemeProvider
(…)
import ThemeContext, {useThemeContext} from "../context/themeContext";
export const ThemeProvider: FC<{children: ReactNode}> = ({ children}) => {
const {theme, toggleTurn} = useThemeContext();
return (
<ThemeContext.Provider value={
{
theme,
toggleTurn
}
}>
{ children }
</ThemeContext.Provider>
)
}
export default ThemeProvider;
App.tsx
(…)
import Template from "./component/template";
import {ThemeProvider} from "./component/provider/theme.provider";
function App() {
return (
<ThemeProvider>
<Template />
</ThemeProvider>
)
}
export default App
Template.tsx
(…)
const Template: FC = (): ReactElement<any, any> | null => {
const { theme, toggleTurn } = useThemeContext();
const toggleTheme = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (toggleTurn) {
toggleTurn();
console.log(theme);
}
};
return (
(…)
<button type="button" onClick={
(e: React.MouseEvent<HMTLButtonElement) =>
toggleTheme(e)
}
(…)
)
I want to create certain conditions based on whether the type of material ui theme is light or dark
How can I do that?
You need to recreate the theme and update the value theme.palette.type ('light' or 'dark') and pass it to the ThemeProvider to apply the change.
const defaultTheme = createMuiTheme({
palette: {
type: "light"
}
});
function App() {
const [theme, setTheme] = React.useState(defaultTheme);
const onClick = () => {
const isDark = theme.palette.type === "dark";
setTheme(
createMuiTheme({
palette: {
type: isDark ? "light" : "dark"
}
})
);
};
return (
<ThemeProvider theme={theme}>
<Card>
<Typography variant="h3">{theme.palette.type}</Typography>
<Button onClick={onClick}>Toggle theme</Button>
</Card>
</ThemeProvider>
);
}
You can then check the theme type in children components using either hook or HOC
Hook
const isDarkTheme = useTheme().palette.type === 'dark';
HOC
const ThemedComponent = withTheme(Component)
render() {
const isDarkTheme = this.props.theme.palette.type === 'dark';
return (...)
}
Live Demo
The following does not work anymore:
const isDarkTheme = useTheme().palette.type === 'dark';
Try this Instead:
const isDarkTheme = useTheme().palette.mode === 'dark';
I can't follow the documentation of implementing Material UI's media queries because it's specified for a plain React app and I'm using NextJs. Specifically, I don't know where to put the following code that the documentation specifies:
import ReactDOMServer from 'react-dom/server';
import parser from 'ua-parser-js';
import mediaQuery from 'css-mediaquery';
import { ThemeProvider } from '#material-ui/core/styles';
function handleRender(req, res) {
const deviceType = parser(req.headers['user-agent']).device.type || 'desktop';
const ssrMatchMedia = query => ({
matches: mediaQuery.match(query, {
// The estimated CSS width of the browser.
width: deviceType === 'mobile' ? '0px' : '1024px',
}),
});
const html = ReactDOMServer.renderToString(
<ThemeProvider
theme={{
props: {
// Change the default options of useMediaQuery
MuiUseMediaQuery: { ssrMatchMedia },
},
}}
>
<App />
</ThemeProvider>,
);
// …
}
The reason that I want to implement this is because I use media queries to conditionally render certain components, like so:
const xs = useMediaQuery(theme.breakpoints.down('sm'))
...
return(
{xs ?
<p>Small device</p>
:
<p>Regular size device</p>
}
)
I know that I could use Material UI's Hidden but I like this approach where the media queries are variables with a state because I also use them to conditionally apply css.
I'm already using styled components and Material UI's styles with SRR. This is my _app.js
import NextApp from 'next/app'
import React from 'react'
import { ThemeProvider } from 'styled-components'
const theme = {
primary: '#4285F4'
}
export default class App extends NextApp {
componentDidMount() {
const jssStyles = document.querySelector('#jss-server-side')
if (jssStyles && jssStyles.parentNode)
jssStyles.parentNode.removeChild(jssStyles)
}
render() {
const { Component, pageProps } = this.props
return (
<ThemeProvider theme={theme}>
<Component {...pageProps} />
<style jsx global>
{`
body {
margin: 0;
}
.tui-toolbar-icons {
background: url(${require('~/public/tui-editor-icons.png')});
background-size: 218px 188px;
display: inline-block;
}
`}
</style>
</ThemeProvider>
)
}
}
And this is my _document.js
import React from 'react'
import { Html, Head, Main, NextScript } from 'next/document'
import NextDocument from 'next/document'
import { ServerStyleSheet as StyledComponentSheets } from 'styled-components'
import { ServerStyleSheets as MaterialUiServerStyleSheets } from '#material-ui/styles'
export default class Document extends NextDocument {
static async getInitialProps(ctx) {
const styledComponentSheet = new StyledComponentSheets()
const materialUiSheets = new MaterialUiServerStyleSheets()
const originalRenderPage = ctx.renderPage
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props =>
styledComponentSheet.collectStyles(
materialUiSheets.collect(<App {...props} />)
)
})
const initialProps = await NextDocument.getInitialProps(ctx)
return {
...initialProps,
styles: [
<React.Fragment key="styles">
{initialProps.styles}
{materialUiSheets.getStyleElement()}
{styledComponentSheet.getStyleElement()}
</React.Fragment>
]
}
} finally {
styledComponentSheet.seal()
}
}
render() {
return (
<Html lang="es">
<Head>
<link
href="https://fonts.googleapis.com/css?family=Comfortaa|Open+Sans&display=swap"
rel="stylesheet"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
First a caveat -- I do not currently have any experience using SSR myself, but I have deep knowledge of Material-UI and I think that with the code you have included in your question and the Next.js documentation, I can help you work through this.
You are already showing in your _app.js how you are setting your theme into your styled-components ThemeProvider. You will also need to set a theme for the Material-UI ThemeProvider and you need to choose between two possible themes based on device type.
First define the two themes you care about. The two themes will use different implementations of ssrMatchMedia -- one for mobile and one for desktop.
import mediaQuery from 'css-mediaquery';
import { createMuiTheme } from "#material-ui/core/styles";
const mobileSsrMatchMedia = query => ({
matches: mediaQuery.match(query, {
// The estimated CSS width of the browser.
width: "0px"
})
});
const desktopSsrMatchMedia = query => ({
matches: mediaQuery.match(query, {
// The estimated CSS width of the browser.
width: "1024px"
})
});
const mobileMuiTheme = createMuiTheme({
props: {
// Change the default options of useMediaQuery
MuiUseMediaQuery: { ssrMatchMedia: mobileSsrMatchMedia }
}
});
const desktopMuiTheme = createMuiTheme({
props: {
// Change the default options of useMediaQuery
MuiUseMediaQuery: { ssrMatchMedia: desktopSsrMatchMedia }
}
});
In order to choose between the two themes, you need to leverage the user-agent from the request. Here's where my knowledge is very light, so there may be minor issues in my code here. I think you need to use getInitialProps (or getServerSideProps in Next.js 9.3 or newer). getInitialProps receives the context object from which you can get the HTTP request object (req). You can then use req in the same manner as in the Material-UI documentation example to determine the device type.
Below is an approximation of what I think _app.js should look like (not executed, so could have minor syntax issues, and has some guesses in getInitialProps since I have never used Next.js):
import NextApp from "next/app";
import React from "react";
import { ThemeProvider } from "styled-components";
import { createMuiTheme, MuiThemeProvider } from "#material-ui/core/styles";
import mediaQuery from "css-mediaquery";
import parser from "ua-parser-js";
const theme = {
primary: "#4285F4"
};
const mobileSsrMatchMedia = query => ({
matches: mediaQuery.match(query, {
// The estimated CSS width of the browser.
width: "0px"
})
});
const desktopSsrMatchMedia = query => ({
matches: mediaQuery.match(query, {
// The estimated CSS width of the browser.
width: "1024px"
})
});
const mobileMuiTheme = createMuiTheme({
props: {
// Change the default options of useMediaQuery
MuiUseMediaQuery: { ssrMatchMedia: mobileSsrMatchMedia }
}
});
const desktopMuiTheme = createMuiTheme({
props: {
// Change the default options of useMediaQuery
MuiUseMediaQuery: { ssrMatchMedia: desktopSsrMatchMedia }
}
});
export default class App extends NextApp {
static async getInitialProps(ctx) {
// I'm guessing on this line based on your _document.js example
const initialProps = await NextApp.getInitialProps(ctx);
// OP's edit: The ctx that we really want is inside the function parameter "ctx"
const deviceType =
parser(ctx.ctx.req.headers["user-agent"]).device.type || "desktop";
// I'm guessing on the pageProps key here based on a couple examples
return { pageProps: { ...initialProps, deviceType } };
}
componentDidMount() {
const jssStyles = document.querySelector("#jss-server-side");
if (jssStyles && jssStyles.parentNode)
jssStyles.parentNode.removeChild(jssStyles);
}
render() {
const { Component, pageProps } = this.props;
return (
<MuiThemeProvider
theme={
pageProps.deviceType === "mobile" ? mobileMuiTheme : desktopMuiTheme
}
>
<ThemeProvider theme={theme}>
<Component {...pageProps} />
<style jsx global>
{`
body {
margin: 0;
}
.tui-toolbar-icons {
background: url(${require("~/public/tui-editor-icons.png")});
background-size: 218px 188px;
display: inline-block;
}
`}
</style>
</ThemeProvider>
</MuiThemeProvider>
);
}
}
MUI v5
You'd need two packages,
ua-parser-js - to parse the user agent device type. With this we can know whether a user is on mobile or desktop.
css-mediaquery - to provide an implementation of matchMedia to the useMediaQuery hook we have used everywhere.
// _app.js
import NextApp from 'next/app';
import parser from 'ua-parser-js'; // 1.
import mediaQuery from 'css-mediaquery'; // 2.
import { createTheme } from '#mui/material';
const App = ({ Component, pageProps, deviceType }) => {
const ssrMatchMedia = (query) => ({
matches: mediaQuery.match(query, {
// The estimated CSS width of the browser.
width: deviceType === 'mobile' ? '0px' : '1024px',
}),
});
const theme = createTheme({
// your MUI theme configuration goes here
components: {
MuiUseMediaQuery: {
defaultProps: {
ssrMatchMedia,
},
},
}
});
return (
<ThemeProvider theme={theme}>
<Component {...pageProps} />
</ThemeProvider>
);
};
App.getInitialProps = async (context) => {
let deviceType;
if (context.ctx.req) {
deviceType = parser(context.ctx.req.headers['user-agent']).device.type || 'desktop';
}
return {
...NextApp.getInitialProps(context),
deviceType,
};
};
export default App;
Hello I have a custom hook to define my theme
code:
export default function App() {
const [theme, setTheme] = usePersistedState('light');
const toggleTheme = () => {
setTheme(theme.type === 'light' ? 'dark' : 'light');
};
return (
<ThemeProvider theme={theme}>
<GlobalStyle />
<div className="App">
<button onClick={toggleTheme}>a</button>
</div>
</ThemeProvider>
);
and here is my hook:
import { darkTheme, lightTheme } from '../themes/index';
function usePersistedState(key) {
const [state, setState] = useState(() => {
switch (key) {
case 'dark':
return darkTheme;
case 'light':
return lightTheme;
default:
return lightTheme;
}
});
console.log(state);
useEffect(() => {
localStorage.setItem(key, JSON.stringify(state.type));
}, [key, state]);
return [state, setState];
}
export default usePersistedState;
basically every time my state changes I save it to local storage, but for some reason it only works the first time
when I try to change my theme the second time I do not enter my switch
edit:
as i show in the gif the first time i get an object because i enter my switch, but when i try to change the theme i get only a text cuz i dont enter my switch
The argument you provide to useState will apply only on the first render.
On the next renders, what you provide to setState function will be applied (you provide 'light' or 'dark', so state value will be 'light' or 'dark')
For your use case, in which you want the switch case to run every state change, useReducer is a better option than useState (in my opinion).
import { useReducer } from 'react';
import { darkTheme, lightTheme } from '../themes/index';
function usePersistedState(key) {
const [state, dispatch] = useReducer(reducer, null, getTheme(key)); // first argument is the reducer function. second argument is the initial state. third argument is an init function (for complex initialization)
const reducer = (state, action) => { // this will run on every state change (on every dispatch call)
return getTheme(action.type)();
}
const getTheme = (themeType) => () => {
switch (themeType) {
case 'dark':
return darkTheme;
case 'light':
return lightTheme;
default:
return lightTheme;
}
}
console.log(state);
useEffect(() => {
localStorage.setItem('themeType', JSON.stringify(state));
}, [state]);
return [state, dispatch];
}
export default usePersistedState;
And from your component you call dispatch like so:
export default function App() {
const [theme, setTheme] = usePersistedState('light');
const toggleTheme = () => {
setTheme(theme.type === 'light' ? {type: 'dark'} : {type: 'light'});
};
return (
<ThemeProvider theme={theme}>
<GlobalStyle />
<div className="App">
<button onClick={toggleTheme}>a</button>
</div>
</ThemeProvider>
);
I think you forgot to add the object wrapper on the toogle with { type: ... }
const toggleTheme = () => {
setTheme({type: theme.type === 'light' ? 'dark' : 'light'});
};