When migrating to Material-UI v5, how to deal with conditional classes? - reactjs

In the official migration guide, they give the following example of changing the code from JSS (makeStyles) to the new styled mode.
Before:
const useStyles = makeStyles((theme) => ({
background: theme.palette.primary.main,
}));
function Component() {
const classes = useStyles();
return <div className={classes.root} />
}
After:
const MyComponent = styled('div')(({ theme }) =>
({ background: theme.palette.primary.main }));
function App(props) {
return (
<ThemeProvider theme={theme}>
<MyComponent {...props} />
</ThemeProvider>
);
}
This is fine for a single class, but what to do when the code has conditional classes?
e.g.
<main className={classnames(content, open ? contentOpen : contentClosed)}>
{/* content goes here */}
</main>
Here, content, contentOpen, and contentClosed are classes returned from useStyles, but contentOpen and contentClosed are rendered conditionally based on the value of open.
With the new styled method, instead of generating class names we're generating components. Is there a way to elegantly replicate the rendering without resorting to content duplication in the ternary expression?
e.g. we don't want to do something like:
function App(props) {
return (
<ThemeProvider theme={theme}>
{open
? <MyOpenComponent {...props}>{/* content */}</MyOpenComponent>
: <MyClosedComponent {...props}>{/* content */}</MyClosedComponent>
</ThemeProvider>
);
}

There are quite a few possible ways to deal with this. One approach using styled is to leverage props to do dynamic styles rather than trying to use multiple classes.
Here's an example:
import React from "react";
import Button from "#mui/material/Button";
import { styled } from "#mui/material/styles";
const StyledDiv = styled("div")(({ open, theme }) => {
const color = open
? theme.palette.primary.contrastText
: theme.palette.secondary.contrastText;
return {
backgroundColor: open
? theme.palette.primary.main
: theme.palette.secondary.main,
color,
padding: theme.spacing(0, 1),
"& button": {
color
}
};
});
export default function App() {
const [open, setOpen] = React.useState(false);
return (
<StyledDiv open={open}>
<h1>{open ? "Open" : "Closed"}</h1>
<Button onClick={() => setOpen(!open)}>Toggle</Button>
</StyledDiv>
);
}
Here's an equivalent example using TypeScript:
import * as React from "react";
import Button from "#mui/material/Button";
import { styled } from "#mui/material/styles";
const StyledDiv: React.ComponentType<{ open: boolean }> = styled("div")(
({ open, theme }) => {
const color = open
? theme.palette.primary.contrastText
: theme.palette.secondary.contrastText;
return {
backgroundColor: open
? theme.palette.primary.main
: theme.palette.secondary.main,
color,
padding: theme.spacing(0, 1),
"& button": {
color
}
};
}
);
export default function App() {
const [open, setOpen] = React.useState(false);
return (
<StyledDiv open={open}>
<h1>{open ? "Open" : "Closed"}</h1>
<Button onClick={() => setOpen(!open)}>Toggle</Button>
</StyledDiv>
);
}
Some other possible approaches:
Use Emotion's css prop and Emotion's capabilities for composing styles
Use tss-react to retain similar syntax to makeStyles but backed by Emotion (so you wouldn't be including both Emotion and JSS in your bundle as would be the case if you leverage makeStyles from #material-ui/styles). This is the route I took when migrating to v5 and as part of my migration I created a codemod for migrating JSS makeStyles to tss-react's makeStyles.

Related

React Beginner - Trouble understanding useContext for dark mode

I'm completing an online program to learn ReactJS. After going over useState we are now learning useContext. Below I'll go over my current understanding of how useContext works, and where I'm facing trouble.
The goal is a simple page with a light/dark mode switch
What I currently understand as the "steps" to using useContext:
Import and initialize createContext
Wrap child components with Provider
Import useContext hook from React so we can use the Context in child components
Access the user Context in desired component(s)
But I'm facing an issue with understanding the code block below
This is the solution to a file named ThemeContext.js.
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext(undefined);
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider
value={{
theme,
toggleTheme: () => setTheme(theme === "light" ? "dark" : "light"),
}}
>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => useContext(ThemeContext);
This is the solution to App.js.
import "./App.css";
import { ThemeProvider, useTheme } from "./ThemeContext";
import Switch from "./Switch";
const Title = ({ children }) => {
const { theme } = useTheme();
return (
<h2
style={{
color: theme === "light" ? "black" : "white",
}}
>
{children}
</h2>
);
};
const Paragraph = ({ children }) => {
const { theme } = useTheme();
return (
<p
style={{
color: theme === "light" ? "black" : "white",
}}
>
{children}
</p>
);
};
const Content = () => {
return (
<div>
<Paragraph>
We are a pizza loving family. And for years, I searched and searched and
searched for the perfect pizza dough recipe. I tried dozens, or more.
And while some were good, none of them were that recipe that would
make me stop trying all of the others.
</Paragraph>
</div>
);
};
const Header = () => {
return (
<header>
<Title>Little Lemon 🍕</Title>
<Switch />
</header>
);
};
const Page = () => {
return (
<div className="Page">
<Title>When it comes to dough</Title>
<Content />
</div>
);
};
function App() {
const { theme } = useTheme();
return (
<div
className="App"
style={{
backgroundColor: theme === "light" ? "white" : "black",
}}
>
<Header />
<Page />
</div>
);
}
function Root() {
return (
<ThemeProvider>
<App />
</ThemeProvider>
);
}
export default Root;
Finally, this is the solution to index.js
import "./Styles.css";
import { useTheme } from "../ThemeContext";
const Switch = () => {
const { theme, toggleTheme } = useTheme();
return (
<label className="switch">
<input
type="checkbox"
checked={theme === "light"}
onChange={toggleTheme}
/>
<span className="slider round" />
</label>
);
};
export default Switch;
My questions begin here
Instead of directly wrapping children with Provider, they instead create ThemeProvider that then returns ThemeContext.Provider. Why is this? and why is { children } necessary as seen in App.js along with the return ThemeContext return statement?
This exercise goes beyond what I believe was taught in the lesson, so I could have some holes to fill in my knowledge as far as using { children } along with the use of ThemeProvider. Normally it's demonstrated as <ThemeContext.Provider> wrapping children on the inside. In App it looks like they don't do this, but it's done in the Root, and maybe since they're wrapping App that's why { children } is indicated? I'm not certain about this and I'd just like to know why things were done specifically like this (again, this is unlike what was demonstrated in past exercises). First post, thanks in advance.
EDIT: After looking more into this issue I'm starting to come around and understand how they came up with this solution. One of the few things they didn't do previously that was used in this example was the use of ({ children }). This caused confusion for me at first, but I've come closer to understanding its usage. For example, its use in the Paragraph component:
const Paragraph = ({ children }) => {
const { theme } = useTheme();
return (
<p
style={{
color: theme === "light" ? "black" : "white",
}}
>
{children}
</p>
);
};
Which is later referenced in the Content component as such:
<Paragraph>
We are a pizza loving family. And for years, I searched and searched and
searched for the perfect pizza dough recipe. I tried dozens, or more.
And while some were good, none of them were that recipe that would
make me stop trying all of the others.
</Paragraph>
This simply means to take the children of the Paragraph component and return the information styled as such. Whatever comes inside of Paragraph, in this case a block of text, was returned with the intended style. I thought of deleting this post but maybe it will help someone else. Not sure if adding more about what I learned here would be excessive, and I'm still wrapping my head around the rest of the issue so documenting here isn't my top priority as of now.
React Context is a relatively new feature that the React team introduced to enable an alternative to holding state in a component and prop drilling. The context provider is a node that holds a value, which can be accessed by any node rendered under it (any children or children of children) via the createContext() with useContext or Context.Consumer. When the value of the Provider changes, any component subscribed with useContext or rendered by Consumer will be rerendered.
There is no practical difference between using the provider directly in index.js. What matters is who is rendered under the provider. Keep in mind that JSX's <ThemeProvider> is in reality a call to React.createElement with App in the children argument.
Why create a ThemeProvider? It packages the provider with state. The alternative would be to use the provider directly in Root and create useState there, but it is inflexible and not reusable.
const ThemeContext = createContext(undefined);
function Root() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider
value={{
theme,
toggleTheme: () => setTheme(theme === "light" ? "dark" : "light"),
}}
>
<App />
</ThemeContext.Provider>
);
}
Note that App is still the children of the provider.

Material UI override react subtree except for certain components

Is there a way to override the theme of a react sub-tree, but skip the override for certain components?
I'm overriding all children of a component, making sure all the font size is small, using:
const overrideTheme = (theme: Theme): Theme => {
return createTheme({
...theme,
typography: {
fontSize: 11,
}
})
}
const MyCustomThemeWrapper = ({children}) => {
return (
<div>
<ThemeProvider theme={overrideTheme}>
{children}
</ThemeProvider>
</div>
)
}
What I want to do is make this override exclude certain components (and their children). In particular, I don't want to override the font size of any Dialog components that's part of the children subtree. Any way I can do this, without having to "re-override" the theme for each Dialog? I.e. I want something this:
const Component1 = () => {
return (
<MyCustomThemeWrapper>
<div>
This text has fontSize 11
<span> more font size 11</span>
<Dialog open={open}>
<DialogContent>
This text has the "original" fontSize
</DialogContent>
</Dialog>
</div>
</MyCustomThemeWrapper>
)
}
where the custom theme override "hits" all the children of MyCustomThemeWrapper except for the Dialog subtree. I know I'm asking for much here, but would be really nice if this was possible.
You can override inline component's styles with wrapping another ThemeProvider as a parent to inline components. For instance, If you would like to except the Dialog component from override ThemeProvider, You should wrap it with another ThemeProvider. Because of your code wasn't clear enough to describing on it, I've made another example which in that inline checkboxs styles, override by another ThemeProvider:
import React from "react";
import { createTheme, ThemeProvider } from "#material-ui/core/styles";
import Checkbox from "#material-ui/core/Checkbox";
import { green, orange } from "#material-ui/core/colors";
const outerTheme = createTheme({
palette: {
secondary: {
main: orange[500]
}
}
});
const innerTheme = createTheme({
palette: {
secondary: {
main: green[500]
}
}
});
const CustomCheckBox = () => {
return (
<ThemeProvider theme={innerTheme}>
<Checkbox defaultChecked />
</ThemeProvider>
);
};
export default function App() {
return (
<ThemeProvider theme={outerTheme}>
<Checkbox defaultChecked />
<Checkbox defaultChecked />
<CustomCheckBox />
</ThemeProvider>
);
}
Here's the result:

How to access material theme in shared component in react?

I have two react projects Parent and Common project (contains common component like header, footer)
I have material theme defined in Parent and configured in standard way using MuiThemeProvider.
However, this theme object is available inside components defined in Parent, but not in share project common.
Suggestions are appreciated.
Added below more details on Oct 30, 2020
Parent Component
import React from "react";
import "./App.css";
import { BrowserRouter, Switch, Route } from "react-router-dom";
import themeDefault from "./CustomTheme.js";
import { MuiThemeProvider } from "#material-ui/core/styles";
import { createMuiTheme } from "#material-ui/core/styles";
import Dashboard from "./containers/Dashboard/Dashboard";
import { Footer, Header } from "my-common-react-project";
function App() {
const routes = () => {
return (
<BrowserRouter>
<Switch>
<Route exact path="/" component={Dashboard} />
</Switch>
</BrowserRouter>
);
};
return (
<MuiThemeProvider theme={createMuiTheme(themeDefault)}>
<div className="App">
<Header
logo="some-logo"
userEmail={"test#email"}
/>
... app components here..
<Footer />
</div>
</MuiThemeProvider>
);
}
export default App;
Shared component
import React from "react";
import {
Box,
AppBar,
Toolbar,
Typography,
} from "#material-ui/core/";
import styles from "./Header.styles";
import PropTypes from "prop-types";
const Header = (props) => {
const classes = styles();
const { options, history } = props;
const [anchorEl, setAnchorEl] = React.useState(null);
const handleCloseMenu = () => {
setAnchorEl(null);
};
const goto = (url) => {
history.push(url);
};
return (
<Box component="nav" className={classes.headerBox}>
<AppBar position="static" className={classes.headerPart}>
<Toolbar className={classes.toolBar}>
{localStorage && localStorage.getItem("isLoggedIn") && (
<>
{options &&
options.map((option) => (
<Typography
key={option.url}
variant="subtitle1"
className={classes.headerLinks}
onClick={() => goto(option.url)}
>
{option.name}
</Typography>
))}
</>
)}
</Toolbar>
</AppBar>
</Box>
);
};
Header.propTypes = {
options: PropTypes.array
};
export default Header;
Shared Component style
import { makeStyles } from "#material-ui/core/styles";
export default makeStyles((theme) => ({
headerPart: {
background: "white",
boxShadow: "0px 4px 15px #00000029",
opacity: 1,
background: `8px solid ${theme.palette.primary.main}`
borderTop: `8px solid ${theme.palette.primary.main}`
}
}));
The Parent component defined theme.palette.primary.main as say Red color and I expect same to be applied in Header but it is picking a different theme (default) object which has theme.palette.primary.main blue.
Which results in my header to be in blue color but body in read color.
Any suggestion how to configure this theme object so that header too picks the theme.palette.primary.main from parent theme object.
here is the answer for mui V5
import { useTheme } from '#mui/material/styles' // /!\ I fixed a typo from official doc here
function DeepChild() {
const theme = useTheme();
return <span>{`spacing ${theme.spacing}`}</span>;
}
Taken from mui documentation
You can use either useTheme or withTheme to inject the theme object to any nested components inside ThemeProvider.
Use useTheme hook in functional components
Use withTheme HOC in class-based components (which can't use hook)
function DeepChild() {
const theme = useTheme<MyTheme>();
return <span>{`spacing ${theme.spacing}`}</span>;
}
class DeepChildClass extends React.Component {
render() {
const { theme } = this.props;
return <span>{`spacing ${theme.spacing}`}</span>;
}
}
const ThemedDeepChildClass = withTheme(DeepChildClass);
Live Demo

Switch between dark and light theme in non-material ui component

I am trying to introduce a theme switcher in my app. I have a lot of non-material-ui elements that I need the theme to reflect the changes on them.
The code below shows that I have a state that is called darkState that is set to true. The material ui components in my app reflect those changes but for example the div below does not get the dark color of the dark theme. What is that I am doing wrong in here?
import React, { useState } from "react";
import Header from "./components/Header.js";
import TopBar from "./components/TopBar.js";
import Sequence from "./components/Sequence.js";
import SecondaryWindow from "./components/SecondaryWindow.js";
import { MuiThemeProvider, createMuiTheme, makeStyles } from "#material-ui/core/styles";
import "./App.css";
import { MainContextProvider } from "./contexts/mainContext.js";
function App() {
const [darkState, setDarkState] = useState(true);
const palletType = darkState ? "dark" : "light";
const theme = createMuiTheme({
palette: {
secondary: {
main: "#0069ff",
},
type: palletType,
},
});
const useStyles = makeStyles((theme) => ({
root: {
paddingLeft: 80,
height: "100%",
backgroundColor: theme.palette.background.default,
},
}));
const classes = useStyles();
return (
<MuiThemeProvider theme={theme}>
<MainContextProvider>
<div className={classes.root}>
<Header />
<TopBar />
<Sequence />
<SecondaryWindow />
</div>
</MainContextProvider>
</MuiThemeProvider>
);
}
export default App;
Now I know the answer, in my example, the class root is not able to benefit from the custom-created theme that is provided by MuiThemeProvider. Instead, it uses the original theme that comes in Mui. To solve this, I separated that div into a component. This way, the theme context (custom-theme from MuiThemeProvider) can be accessed by the div. This way when I switch DarkState, colors update on Mui components and HTML elements based on the custom theme palette.
import React, { useContext, useState } from "react";
import Header from "./components/Header.js";
import TopBar from "./components/TopBar.js";
import Sequence from "./components/Sequence.js";
import SecondaryWindow from "./components/SecondaryWindow.js";
import { MuiThemeProvider, createMuiTheme, makeStyles } from "#material-ui/core/styles";
import "./App.css";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { MainContextProvider } from "./contexts/mainContext.js";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
function AppContent() {
const useStyles = makeStyles((theme) => ({
root: {
paddingLeft: 80,
height: "100%",
backgroundColor: theme.palette.background.default,
},
}));
const classes = useStyles();
return (
<div className={classes.root}>
<Header />
<TopBar />
<Sequence />
<SecondaryWindow />
</div>
);
}
function App() {
const [darkState, setDarkState] = useState(true);
const palletType = darkState ? "dark" : "light";
const theme = createMuiTheme({
palette: {
secondary: {
main: "#0069ff",
},
type: palletType,
},
});
return (
<MuiThemeProvider theme={theme}>
<MainContextProvider>
<AppContent />
</MainContextProvider>
</MuiThemeProvider>
);
}
export default App;
It's because you only change the #material component not the CSS, to change the CSS Theme, you need to make variable for CSS for Dark Theme.
on :root declare all the light theme color and div.darkmode all the darkmode:
:root {
--color-bg: #fff;
--color-text: #000;
}
.div.darkmode {
--color-bg: #363636;
--color-text: #d1d1d1;
}
/** Usage */
.div {
color: var(--color-text);
background: var(--color-bg)
}
and make a condition on the div when the dark theme is true a new classname darkmode will be added to dive as you wrote above
<div className={`${classes.root} ${darkState && `darkmode`}`}>
<Header />
<TopBar />
<Sequence />
<SecondaryWindow />
</div>
I created an example for you here.
let us know if anything goes wrong!
workaround 2
if you're not doing any customer style by CSS file then this will work
import React from 'react';
import CssBaseline from '#material-ui/core/CssBaseline';
export default function MyApp() {
return (
<MuiThemeProvider theme={theme}>
<CssBaseline />
{/* The rest of your application */}
</MuiThemeProvider>
);
}
I would declare both variations of your theme as constants above the mounting or rendering. This way you are not literally creating a new theme ever time your theme swaps. I would have a state holding the reference to the MUI-Theme constant.
You can manipulate data-theme attribute to toggle the dark/light theme. Try it on StackBlitz.
Setting up themes
Use data-theme attribute to set the selected theme.
/* default theme (light) */
:root {
--primary-color: #302ae6;
--secondary-color: #536390;
--font-color: #424242;
--bg-color: #fff;
}
/* dark theme */
[data-theme='dark'] {
--primary-color: #9a97f3;
--secondary-color: #818cab;
--font-color: #e1e1ff;
--bg-color: #161625;
}
Switching theme in React Hooks
// App.js
const [theme, setTheme] = useState({
light: true,
});
const handleChangeTheme = (event) => {
setTheme({ ...theme, [event.target.name]: event.target.checked });
};
Set our data-theme attribute accordingly
const currentTheme = theme.light === true ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', currentTheme);

React JS // Editing style of one component from another component

My goal is to create a basic app that allows me to change the style of one component with an action from another component.
Lets assume I have a <Btn/> component and a <Box/> component and when the button is clicked, I want to change the background color of the box. <Btn/> and <Box/> have the common ancestor of <App/> but are both at different levels in the component tree.
Btn.js
import React from 'react'
function Btn() {
const handleClick = (e) => {
//...
}
return (
<button onClick={handleClick}>
Click me
</button>
);
}
export default Btn
Box.js
import React from 'react'
function Box() {
return (
<h1>
Hello World!
</h1>
);
}
export default Box
I do not want to use prop drilling (with style setting/getting functionality in the <App/> component) to achieve this. I have also deliberately left out component styling as I am open to whichever styling option is best to solve this problem.
What would be the best way to go about this? (I'm open to using Context, Redux or another library if it is appropriate.)
The simplest way of doing this is with Context, as you're using function components not classes the documentation you'll need is useContext https://reactjs.org/docs/hooks-reference.html#usecontext. You still have to define the prop and "setter" function at the app level or at a component called at the app level, but with context you don't have to pass the props all the way down.
To take their example and adapt it to your use case would go something like this. (Working sample: https://codesandbox.io/s/stackoverflow-answer-7hryk)
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
const [stateTheme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme: themes[stateTheme], setTheme: setStateTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ToggleButtons />
<ThemedButton />
</div>
);
}
function ToggleButtons() {
const { setTheme } = useContext(ThemeContext);
return (
<div>
<button onClick={() => setTheme('light')}>Light Theme</button>
<button onClick={() => setTheme('dark')}>Dark Theme</button>
</div>
);
}
function ThemedButton() {
const { theme } = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}

Resources