I want to add dark mode in my React Native App. I have come across many implementations including React Navigation and Appearance. Everywhere I see code similar to the one below:-
import { Text, useColorScheme } from 'react-native';
const theme = useColorScheme();
<Text style={theme === 'light' ? styles.text_light : styles.text_dark}>Hello</Text>
But using these ternary operators everywhere is adding up to noise in code. I have created my custom Text and View as you can see below.
import { Text, useColorScheme } from 'react-native';
const theme = useColorScheme();
const CustomText = ({ styles, children }) => {
return (
<Text
style={[
theme === 'dark' ? { color: 'white' } : { color: 'black' },
styles,
]}>
{children}
</Text>
);
};
My Question is, Is my solution feasible? Can I replace React Native Components like Text and View(that get used everywhere) with custom components, or the ternary operator in React Components a better way. Thank you.
react-native-paper provides components that are colored using the theme stored in the PaperProvider context. React navigation has a similar context. Since these themes are compatible with one another, here's an example where users allow to manipulate both:
First merge the paper and navigation light and dark schemes:
// React native paper theme is compatible with react-navigation theme
// so merge them
const CombinedDefaultTheme = merge(
PaperDefaultTheme,
NavigationDefaultTheme
);
const CombinedDarkTheme = merge(PaperDarkTheme, NavigationDarkTheme);
Now I create a function that will customize the merged scheme base on a provided color and whether or not dark mode is desired:
// create theme base on provided color and whether in darkmode
export const getTheme = (color, isDarkMode) => {
// use color scheme generator to get colors
const [primary, accent, ...otherColors] =
colorSchemes.getNeutralScheme(color);
const colorPresets = isDarkMode
? darkenColorPresets
: lightenColorPresets;
// get lighter and darker versions of these colors
const primaryPresets = colorPresets(primary);
const accentPresets = colorPresets(accent);
// get default paper& navigation theme
const DefaultTheme = isDarkMode
? CombinedDarkTheme
: CombinedDefaultTheme;
//override theme with changes
const theme = {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
primary,
accent,
primaryPresets,
accentPresets,
otherColors,
},
};
// make background color darker
const onDark = {
background: colorManipulators.blend(primary, "#000", 0.8),
};
// make text lighter version of background color
onDark.text = colorManipulators.blend(onDark.background, "#fff", 0.6);
if (isDarkMode) theme.colors = { ...theme.colors, ...onDark };
return theme;
};
And now put this theme into a piece of state with useReducer:
import React, { useReducer } from "react";
import { useColorScheme } from "react-native";
import { getTheme } from "../colors";
const initialState = {
mainColor: "#0D3FF8",
};
initialState.isDarkMode = false;
initialState.theme = getTheme(
initialState.mainColor,
initialState.isDarkMode
);
const reducer = (state, action) => {
switch (action.type) {
case "setMainColor": {
let newColor = action.payload;
return {
...state,
mainColor: newColor,
theme: getTheme(newColor, state.isDarkMode),
};
}
case "overrideTheme": {
let newTheme = action.payload;
return { ...state, theme: { ...state.theme, ...newTheme } };
}
case "setIsDark": {
const newDark = action.payload;
return {
...state,
isDarkMode: newDark,
theme: getTheme(state.mainColor, newDark),
};
}
}
};
export default function ThemeReducer(stateOverride = {}) {
console.log("here is the state override");
console.log(stateOverride);
const [state, dispatch] = useReducer(reducer, {
...initialState,
isDarkMode: useColorScheme() == "dark",
...stateOverride,
});
return { ...state, dispatch };
}
Related
I have stared investigate MUI lates version and I see that responsive brake points and all other stuff are base screen size.
But we are developing some kind of dashboard as reusable component. And I want to use default Material-ui responsivness, I like how we can in MUI component define override base on breakpoints.
But our Dashboard component and its breakpoints will work just if whole component will be rendered in IFRAME.
Its way in MUI how to solve this problem? Or use somehow container queries?
Finally I thinking about to override MUI theme brake points in container scope base on size of parent container and its size.
Can you point me to solution?
Override of brakepoints could be done like following code but I am afraid about performance.
import React from "react";
import { MuiThemeProvider, createMuiTheme } from "#material-ui/core/styles";
import HeaderComponent from "./header";
import "./App.css";
const values = {
xs: 0,
sm: 426,
md: 960,
lg: 1280,
xl: 1920
};
// here I can do some calculation base on element size
const theme = createMuiTheme({
palette: {
primary: {
main: "#000000"
},
secondary: {
main: "#9f9f9f"
}
},
breakpoints: {
keys: ["xs", "sm", "md", "lg", "xl"],
up: (key) => `#media (min-width:${values[key]}px)`
}
});
function Dashboard() {
return (
<MuiThemeProvider theme={theme}> // define cope theme provider
<div>
<HeaderComponent></HeaderComponent>
</div>
</MuiThemeProvider>
);
}
export default App;
Thanks for your help
The best result for me was something like this:
import { useCallback, useState } from 'react';
import { useEffect, useLayoutEffect } from 'react';
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export enum SizeTo {
DOWN_TO = 'downTo',
UP = 'up',
}
const getMatches = (el: HTMLElement | null | undefined, size: number, option: SizeTo): boolean => {
// Prevents SSR issues
if (typeof window !== 'undefined' && el) {
if (option === SizeTo.DOWN_TO) {
return el.offsetWidth <= size;
}
return el.offsetWidth > size;
}
return false;
};
function useContainerMediaQuery<T extends HTMLElement = HTMLDivElement>(size: number, option: SizeTo): [
(node: T | null) => void,
boolean,
] {
const [ref, setRef] = useState<T | null>(null);
const [matches, setMatches] = useState<boolean>(getMatches(ref, size, option));
// Prevent too many rendering using useCallback
const handleSize = useCallback(() => {
setMatches(getMatches(ref, size, option));
}, [ref?.offsetHeight, ref?.offsetWidth]);
useIsomorphicLayoutEffect(() => {
handleSize();
// Listen matchMedia
if (window) {
window.addEventListener('resize', handleSize);
}
return () => {
if (window) {
window.removeEventListener('resize', handleSize);
}
};
}, [ref?.offsetWidth]);
return [setRef, matches];
}
export default useContainerMediaQuery;
partly extracted from useHooks
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
}
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;
In React, I have a functional component that validates props and implements default props for any non-required props. I'm also using mui's makeStyles to grab the theme object to apply styling to my components.
My question is how does one go about passing the makeStyles theme object down to the defaultProps to avoid hard keying values?
import React from 'react';
import PropTypes from 'prop-types';
import { makeStyles } from '#material-ui/core/styles';
const useStyles = makeStyles(theme => ({
componentStyle: {
color: `${theme.palette.light.main}`, // just like how I'm accessing `theme` here, I'd like to access in `defaultProps`
},
componentContainer: ({ backgroundColor }) => {
return { backgroundColor };
},
}));
const Example = ({ backgroundColor }) => {
const classes = useStyles({ backgroundColor });
return (
<div className={classes.componentStyle} >
<div className={classes.componentContainer} /> // use default styling using `theme` if none is provided
</div>
)
}
Example.propTypes = {
backgroundColor: PropTypes.string,
};
Example.defaultProps = {
backgroundColor: `${theme.palette.light.main}`, // I want to access `theme` here and do the following. While `backgroundColor: 'white'` will work I want to avoid hard keying values.
};
export default Example;
EDIT: based on the solution provided by #Fraction below is what I'll move forward with.
import React from 'react';
import PropTypes from 'prop-types';
import { makeStyles } from '#material-ui/core/styles';
const useStyles = makeStyles(theme => ({
componentStyle: {
color: `${theme.palette.light.main}`,
},
componentContainer: ({ backgroundColor }) => {
return {
backgroundColor: backgroundColor || `${theme.palette.light.main}`
};
},
}));
const Example = ({ backgroundColor }) => {
const classes = useStyles({ backgroundColor });
return (
<div className={classes.componentStyle} >
<div className={classes.componentContainer} />
</div>
)
}
Example.propTypes = {
backgroundColor: PropTypes.string,
};
Example.defaultProps = {
backgroundColor: null,
};
export default Example;
I would suggest to not pass theme as prop, but to use Theme context.
I do that in all apps which I am working on and it is flexible and prevents props drilling as well.
In your top level component, e.g. App.tsx put the Material UI theme provider:
import { ThemeProvider } from '#material-ui/core/styles';
import DeepChild from './my_components/DeepChild';
const theme = {
background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)',
};
function Theming() {
return (
<ThemeProvider theme={theme}>
<DeepChild />
</ThemeProvider>
);
}
Then, in your components which need a theme:
(as per https://material-ui.com/styles/advanced/#accessing-the-theme-in-a-component):
import { useTheme } from '#material-ui/core/styles';
function DeepChild() {
const theme = useTheme();
return <span>{`spacing ${theme.spacing}`}</span>;
}
You don't need to pass the makeStyles's theme object down to the defaultProps, just use the Logical OR || to set the backgroundColor property to theme.palette.light.main when the passed argument is any falsy value, e.g: (0, '', NaN, null, undefined):
const useStyles = makeStyles(theme => ({
componentStyle: {
color: `${theme.palette.light.main}`, // just like how I'm accessing `theme` here, I'd like to access in `defaultProps`
},
componentContainer: ({ backgroundColor }) => ({
backgroundColor: backgroundColor || theme.palette.light.main,
}),
}));
Color-Component that receives colors throw props and does color effect.
export enum Colors {
Blue = "333CFF",
Yellow = "FCFF33",
Green = "33FF36",
}
interface IColorProps {
myColors: Colors ;
}
export const ColorComponent: FunctionComponent<IColorProps > = ( {props: IColorProps } => {
return (
<div className={`colorCalss ${props.bgColor}`} />
)});
ParentComponent that sends props to color-component
import { ColorComponent, Colors } from "ColorComponent.component";
export const ParentComponent: FunctionComponent<IParentComponentProps> =
(props: IParentComponentProps) => {
<ColorComponent myColors={Colors.Blue}/>
}
The below is what I have placed into the IParentComponentProps
export enum Colors {
Blue = "333CFF",
Yellow = "FCFF33",
Green = "33FF36",
}
interface IColorProps {
myColors?: Colors ;
}
export interface IParentComponentProps {
colors: IColorProps;
}
I just started working with typescript.
The above Parent component sends props myColors to ColorComponent and its working I can see blue color being apply on my tests.
How can I send props dynamically to ColorComponent, for example lets say the parent.component props.color get different color that could be apply.
When I do something like: "<"ColorComponent myColors={props.colors}/">", I get error.
How can I get props pass down dynamically using enum?
Your parent interface should have the same color type:
export interface IParentComponentProps {
colors: Colors;
}
This way you will not get the type errors and can pass down the colors prop.
Or pass down the props.colors.myColors variable. But be careful it may not exists as you have it optional.
You don't need to do anything special to send the colors dynamically to the child. Specifically for typescript just make sure all types are consistent.
There's a couple of syntax errors on your example so maybe it's just not compiling for you because of that?
Here's a working sample where parent component dynamically changes the color and sends it to the child every second.
export enum Colors {
Blue = "333CFF",
Yellow = "FCFF33",
Green = "33FF36",
}
interface IColorProps {
color: Colors;
}
export const ColorComponent: React.FunctionComponent<IColorProps> = (props: IColorProps) => {
return <div style={{ backgroundColor: `#${props.color}`, height: '100px', width: '100px' }} />
};
const ParentComponent: React.FC = () => {
const colors = [Colors.Blue, Colors.Green, Colors.Yellow];
const [color, setColor] = React.useState(0);
const { current } = React.useRef({ next: function() { } });
current.next = () => {
const nextColor = (color === 2) ? 0 : (color + 1);
setColor(nextColor);
};
useEffect(
() => {
const interval = setInterval(() => current.next(), 1000);
return () => clearInterval(interval);
},
[]
);
return (
<>
<h1>Current color is #{colors[color]}</h1>
<ColorComponent color={colors[color]}/>
</>
);
}