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,
}),
}));
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 would like to change font size of menu item(material ui v3) in my react app. I am trying to do that using withStyles but I am facing errors. I have a functional component and here is my code.
const styles = theme => ({
menu: {
fontSize: '4rem',
},
});
interface Props {
lang: string;
rate: string;
}
export const withStyles( styles )(App): React.FC<Props> = (props) => {
const { classes } = props;
let search = props.id ? false : true;
return <MenuItem className={classes.menu}>Foo</MenuItem>;
};
Can someone help to use withStyles correctly in the functional component?
This should work for Material UI V3:
import { withStyles, createStyles, Theme, MenuItem } from "#material-ui/core";
const styles = (theme: Theme) =>
createStyles({
menu: {
fontSize: "4rem"
}
});
interface IProps {
classes: {
menu: string,
},
someProp?: string,
lang?: string;
rate?: string;
}
export const App = withStyles(styles)((props: IProps) => {
return (
<>
<MenuItem className={props.classes.menu}>Foo</MenuItem>
<p>{props.someProp}</p>
</>
);
});
To give props is pretty usual stuff, in case of my example it is:
<App someProp="data from prop" />
Sandbox
I have created a separate file for classes prop for e.g. MuiAlert
What is the way to tell makeStyles that you are only allowed to use Alert classes?
The following works but I am sure there must be a better way. So e.g. If I rename root to roott, I will get error that 'roott' does not exist in type 'Partial<Record<AlertClassKey, any>>'
Playground example: https://codesandbox.io/s/typescript-react-material-ui-3t7ln?file=/src/index.ts
import { Theme, makeStyles } from "#material-ui/core";
import { AlertClassKey } from "#material-ui/lab/Alert";
export const useAlertClasses = makeStyles<Theme>(
(): Partial<Record<AlertClassKey, any>> => ({
root: {
borderRadius: 3,
}
}));
my solution:
import { Theme } from "#material-ui/core/styles/createMuiTheme";
import { StyleRules } from "#material-ui/core/styles/withStyles";
import { makeStyles } from "#material-ui/core/styles";
import { createStyles } from "#material-ui/core/styles";
import { AlertClassKey } from "#material-ui/lab/Alert";
export const alertOverrides = (
theme: Theme
): Partial<StyleRules<AlertClassKey>> => {
return {
root: {
backgroundColor: "red !important",
},
};
};
export const useAlertStyles = makeStyles<Theme, {}>(
(theme) => {
return createStyles(alertOverrides(theme) as never);
},
{ name: "MuiAlert" }
);
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;
Points to consider:
The example is made up but illustrates the problem.
In actual application global storage is used and action changing is being emitted inside hover() method of itemTarget. Here, to imitate global
storage, window object is used.
Using ES7 decorators (or other
ES7 syntax) is not allowed.
So, the problem is that in implementation below, when dragging, endDrag() method of itemSource is not being called.
Possible solutions would be create different (but practically the same) components which differ just by item types, import those components to the Container component and mount depending on props.itemType – so, it's not an DRY option.
The questions are:
1. How to do it right? How to reuse and render a draggable component which have depended on Container's props itemType inside DragSource/DropTarget?
2. Why does the solution below not work? Why is the endDrag() method not being called?
Container.js:
import React, { Component } from 'react';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import Item from './Item';
import ItemDndDecorator from './ItemDndDecorator';
const style = {
width: 333,
};
class Container extends Component {
state = {some: true}
hoverHandle = () => {
this.setState({some: !this.state.some})
}
render() {
const Item1 = ItemDndDecorator(Item, 'item1')
const Item2 = ItemDndDecorator(Item, 'item2')
window.hoverHandle = this.hoverHandle
return (
<div style={style}>
<Item1>
<Item2>
some text 1
</Item2>
</Item1>
</div>
);
}
}
export default DragDropContext(HTML5Backend)(Container)
Item.js:
import React from 'react';
const style = {
border: '1px dashed gray',
padding: '1rem',
margin: '1rem',
cursor: 'move',
};
function Item(props) {
const { connectDragSource, connectDropTarget } = props;
return connectDragSource(connectDropTarget(
<div style={style}>
{props.children}
</div>,
));
}
export default Item
ItemDnDDecorator.js:
import { DragSource, DropTarget } from 'react-dnd';
const itemSource = {
beginDrag(props) {
console.log('begin drag');
return { id: props.id } ;
},
endDrag() {
console.log('end drag');
}
};
const itemTarget = {
hover() {
window.hoverHandle()
}
};
function ItemDndDecorator(component, itemType) {
return (
DropTarget(itemType, itemTarget, connect => ({
connectDropTarget: connect.dropTarget(),
}))(
DragSource(itemType, itemSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
}))(component))
)
}
export default ItemDndDecorator