Styled-components and theming with NextJS - reactjs

I try to create a Theme for all of my components in the app, I tried to but failed with /pages/_app.js:
import App, { Container } from "next/app"; // eslint-disable-line
import React from "react";
import { ThemeProvider } from "styled-components";
export default class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps };
}
render() {
const { Component, pageProps } = this.props;
return (
<Container>
<ThemeProvider theme={{ color: 'blue' }}>
<Component {...pageProps} />
</ThemeProvider>
</Container>
);
}
}
But in my components theme prop is always an empty Object:
const A = styled.a`
color: ${props => {
console.log(props);
return "blue";
}};
`;

It should be
const A = styled.a`
color: ${props => props.theme.color};
`;

Related

Prop `className` did not match Server and Client

I followed many solutions like this https://stackoverflow.com/a/71081567/19460332 and the official solution for this https://github.com/mui/material-ui/blob/master/examples/nextjs but nothing worked for me. I am using nextjs , material ui and js-cookies
the issue I am facing is for the MUI Switch
have a look to my code here https://github.com/MaheshYadavGitHub/pizza-gallery:
Here is the code for the section that is breaking :
below is the Layout.js where the Switch to toggle darkMode is not working as expected. The darkTheme gets applied as it gets from the cookies but the switch button still stays in off position on the first render but after another render it works perfectly and when again manually refreshed it does the same behaviour :-
import { useState, useContext } from "react";
import {
Container,
AppBar,
Toolbar,
Typography,
Link,
Switch,
CssBaseline,
ThemeProvider,
createTheme,
} from "#material-ui/core";
import { red } from "#material-ui/core/colors";
import Head from "next/head";
import Image from "next/image";
import NextLink from "next/link";
import useStyles from "../utils/styles";
import { Store } from "../utils/Store";
import Cookies from "js-cookie";
const Layout = ({ title, children }) => {
const { state, dispatch } = useContext(Store);
const { darkMode } = state;
const classes = useStyles();
const theme = createTheme({
palette: {
type: darkMode ? "dark" : "light",
primary: {
main: "#556cd6",
},
secondary: {
main: "#19857b",
},
error: {
main: red.A400,
},
},
typography: {
fontFamily: ["Arial", "Roboto Condensed"].join(","),
fontWeight: 700,
h1: {
fontSize: "1.6rem",
margin: "1rem 0rem",
fontWeight: 600,
"#media (min-width:600px)": {
fontSize: "2.6rem",
},
},
h6: {
fontWeight: 700,
},
navLinks: {
color: "#fff",
},
},
});
const handleThemeChange = (event) => {
dispatch({ type: darkMode ? "DARK_MODE_OFF" : "DARK_MODE_ON" });
const newThemeMode = !darkMode;
Cookies.set("darkMode", newThemeMode ? "ON" : "OFF");
};
return (
<>
<Head>
<title>{title ? `${title} - Pizza Gallery` : "Pizza Gallery"}</title>
</Head>
<ThemeProvider theme={theme}>
<CssBaseline />
<AppBar position="static" className={classes.navbar}>
<Toolbar>
<NextLink href="/" passHref>
<Link>
<Typography
variant="h6"
className={classes.brand}
component="div"
>
Pizza Gallery
</Typography>
</Link>
</NextLink>
<div className={classes.grow}></div>
<Switch
id="switch"
checked={darkMode}
onChange={handleThemeChange}
color="primary"
></Switch>
<NextLink href="/login" passHref>
<Link>
<Typography>Login</Typography>
</Link>
</NextLink>
<NextLink href="/cart" passHref>
<Link>
<Typography>Cart</Typography>
</Link>
</NextLink>
</Toolbar>
</AppBar>
<Container className={classes.main}>{children}</Container>
<footer className={classes.footer}>
<Typography>all rights reserved © pizza gallery 2022</Typography>
</footer>
</ThemeProvider>
</>
);
};
export default Layout;
below is the Store :-
import Cookies from "js-cookie";
import { createContext, useReducer } from "react";
export const Store = createContext();
const initialState = {
darkMode: Cookies.get("darkMode") === "ON" ? true : false,
};
const reducer = (state, action) => {
switch (action.type) {
case "DARK_MODE_ON":
return { ...state, darkMode: true };
case "DARK_MODE_OFF":
return { ...state, darkMode: false };
default:
return state;
}
};
export const StoreProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const value = { state, dispatch };
return <Store.Provider value={value}>{children}</Store.Provider>;
};
/pages/_app.js
import { useEffect } from "react";
import "../styles/globals.css";
import { StoreProvider } from "../utils/Store";
function MyApp({ Component, pageProps }) {
useEffect(() => {
const jssStyles = document.querySelector("#jss-server-side");
if (jssStyles) {
jssStyles.parentElement.removeChild(jssStyles);
}
}, []);
return (
<StoreProvider>
<Component {...pageProps} />
</StoreProvider>
);
}
export default MyApp;
/pages/_document.js
import { ServerStyleSheets } from "#material-ui/core/styles";
import Document, { Html, Head, Main, NextScript } from "next/document";
import React from "react";
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head></Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
MyDocument.getInitialProps = async (ctx) => {
const sheets = new ServerStyleSheets();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () => {
return originalRenderPage({
enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
});
};
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: [
...React.Children.toArray(initialProps.styles),
sheets.getStyleElement(),
],
};
};
If you're not using Server Side Rendering for that component, you may try importing the component, by turning off ssr:
import dynamic from 'next/dynamic';
const Component = dynamic(() =>
import('../components/path_to_component'), {
ssr: false,
});
Hope this helps!

useContext passing wrong props

I have my context Provider in my AppContext.tsx file
import { useState, useEffect, useContext, createContext } from 'react';
interface AppContextProps {
children: React.ReactNode
}
const themes = {
dark: {
backgroundColor: 'black',
color: 'white'
},
light: {
backgroundColor: 'white',
color: 'black'
}
}
const initialState = {
dark: false,
theme: themes.light,
toggle: () => { console.log('toggle from initalState') }
}
const AppContext = createContext(initialState);
export function AppWrapper({ children }: AppContextProps) {
const [dark, setDark] = useState(false) // Default theme is light
// On mount, read the preferred theme from the persistence
useEffect(() => {
const isDark = localStorage.getItem('dark') === 'true'
setDark(isDark)
}, [dark])
// To toggle between dark and light modes
const toggle = () => {
console.log('toggle from AppWrapper')
const isDark = !dark
localStorage.setItem('dark', JSON.stringify(isDark))
setDark(isDark)
}
const theme = dark ? themes.dark : themes.light
return (
<AppContext.Provider value={{ theme, dark, toggle }}>
{children}
</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
I insert Appwrapper into my _app.tsx file
import type { ReactElement, ReactNode } from 'react'
import type { NextPage } from 'next'
import type { AppProps } from 'next/app'
import { AppWrapper } from '../contexts/AppContext';
import 'normalize.css/normalize.css';
import '../styles/globals.scss'
type NextPageWithLayout = NextPage & {
getLayout?: (page: ReactElement) => ReactNode
}
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout
}
const App = ({ Component, pageProps }: AppPropsWithLayout) => {
const getLayout = Component.getLayout ?? ((page) => page)
return getLayout(
<AppWrapper>
<Component {...pageProps} />
</AppWrapper>
)
}
export default App
And I call and import the useAppContext hook to use it in my Header component
import styles from './header.module.scss'
import { useAppContext } from '../contexts/AppContext'
const Header = () => {
const { theme, toggle, dark } = useAppContext()
return (
<header className={styles.header}>
<nav className={styles.nav}>
<div onClick={toggle} className={styles.switchWrap}>
<label className={styles.switch}>
<input type="checkbox" />
<span className={styles.slider}></span>
</label>
</div>
</nav>
</header>
)
}
export default Header
However, my toggle function logs toggle from initalState instead of toggle from AppWrapper.
The following code below
const { theme, toggle, dark } = useAppContext()
gets data from initialState
const initialState = {
dark: false,
theme: themes.light,
toggle: () => { console.log('toggle from initalState') }
}
instead of what I pass into the value props shown below
<AppContext.Provider value={{ theme, dark, toggle }}>
{children}
</AppContext.Provider>
How do I correctly pass the value props data into my component instead of the data from initialState?
Because my app was using per-page layouts, I inserted AppWrapper in my layout file instead of _app.tsx and everything works
import Header from './header'
import Footer from './footer'
import { AppWrapper } from '../contexts/AppContext';
interface LayoutProps {
children: React.ReactNode
}
const Layout = ({ children }: LayoutProps) => {
return (
<AppWrapper>
<Header />
{children}
<Footer />
</AppWrapper>
)
}
export const getLayout = (page: LayoutProps) => <Layout>{page}</Layout>;
export default Layout

Passing styles from parent to mui component in react

Hi I have the following React component that positions its children with styles.
const styles = () => ({
bathButt : {
top :278,
left : 336
},
})
class AudioZones extends Component {
render() {
const { classes } = this.props;
return (
<IconButton className={classes.bathButt} >
<Speaker/>
</IconButton>
);
}
}
export default withStyles(styles) (AudioZones);
I have created a child component "AudioZone"
render()
return (
);
}
which i substitute into the parent
render() {
const { classes } = this.props;
return (
<AudioZone/> );
}
However I have run into trouble on how I pass down the "bathButt" style so that the position of the button is set in the parent but read and rendered by the child.
Any help appreciated
For withStyles you can use Higher-Order Components (HOC) to pass styles from parent to child
const styles = () => ({
bathButt: {
top: 20,
left: 30,
backgroundColor: "blue"
}
});
const withMyStyles = WrappedComponent => {
const WithStyles = ({ classes }) => {
return (
<div>
<WrappedComponent classes={classes} />
</div>
);
};
return withStyles(styles)(WithStyles);
};
and use it in your child component
class AudioZones extends Component {
render() {
const { classes } = this.props;
return (
<IconButton className={classes.bathButt}>
<h1>Speaker Component</h1>
</IconButton>
);
}
}
export default withMyStyles(AudioZones);
but insted of withStyles you can use makeStyles,i think its easer
const useStyles = makeStyles({
bathButt: { top: 20, left: 50, color: "red" } // a style rule
});
function App(props) {
return <AudioZones useStyles={useStyles} />;
}
child component
function AudioZones(props) {
const classes = useStyles();
return (
<div>
<IconButton className={classes.bathButt}>
<h1>Speaker Component</h1>
</IconButton>
</div>
);
}
Working Codesandbox for withStyles and makeStyles

Change a prop on a styled component and update appearance

I'm very new to styled components (and I'm not great with React in general) and I can't quite get my head around them. I've created a basic example which is an abstraction of what I want to achieve. When I click on the box, I want the property on to be changed to true and for the colour of <Box> to be updated to green as per the background-color rule.
How do I achieve this? Especially in the instance where there could be an indeterminate number of boxes?
Component
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
const Box = styled.a`
display: block;
height: 100px;
width: 100px;
background-color: ${props => props.on ? 'green' : 'red' };
`;
Box.propTypes = {
on: PropTypes.bool,
onClick: PropTypes.func,
}
Box.defaultProps = {
on: false,
onClick: () => {},
}
export default Box;
Implementation
<Box on={ false } onClick={ }></Box>
// App.js
import React from "react";
import ReactDOM from "react-dom";
import Test from "./Test";
class App extends React.Component {
state = {
on: false
};
handleChange = () => {
this.setState(prevState => ({ on: !prevState.on }));
};
render() {
return (
<div className="App">
<Test on={this.state.on} onClick={this.handleChange}>
Hey
</Test>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
// Test.js
import PropTypes from "prop-types";
import styled from "styled-components";
const Box = styled.a`
display: block;
height: 100px;
width: 100px;
background-color: ${props => (props.on ? "green" : "red")};
`;
Box.propTypes = {
on: PropTypes.bool,
onClick: PropTypes.func
};
Box.defaultProps = {
on: false,
onClick: () => {}
};
export default Box;
You would handle the state in its parent component. For example you could do something like this:
class Page extends Component {
state = { on: false }
handleClick = () => {
this.setState(prevState => ({ on: !prevState.on }));
}
render() {
return <Box on={this.state.on} onClick={this.handleClick} />
}
}
Or even simpler using React hooks:
const Page = () => {
const [on, setOn] = useState(false);
return <Box on={on} onClick={() => setOn(on => !on)} />;
};
Here's an example of what you could do if you wanted 10 boxes
(note: creating the onClick handler in the render method like I did could cause performance issues if you have a very large number of boxes)
class Page extends Component {
state = { boxes: Array(10).fill(false) }
handleClick = (index) => {
this.setState(prevState => ({
boxes: [
...prevState.boxes.slice(0, index),
!prevState.boxes[index],
...prevState.boxes.slice(index)
]
}));
}
render() {
return (
<React.Fragment>
{this.state.boxes.map((on, index) =>
<Box on={on} onClick={() => this.handleClick(index)} />
)}
</React.Fragment>
);
}
}

React Material UI Theme Change

can you please help me to change the React Material UI theme Dynamically .
https://imgur.com/S8zsRPQ
https://imgur.com/Ul8J40N
I have tried by changing the theme Properties on button click . The theme properties are getting changed as seen in the console . But the change is not reflecting on the theme .
Sandbox Code : https://codesandbox.io/s/30qwyk92kq
const themeChange = () => {
alert(theme);
theme.palette.type = "light";
console.log(theme);
};
ReactDOM.render(
<MuiThemeProvider theme={theme}>
<React.Fragment>
<CssBaseline />
<App changeTheme={themeChange} />
</React.Fragment>
</MuiThemeProvider>,
document.getElementById("app")
);
When I click the button the theme has to change to Dark color
I am using styledComponents, typescript and material-ui.
First I defined my themes:
// This is my dark theme: dark.ts
// I defined a light.ts too
import createMuiTheme from '#material-ui/core/styles/createMuiTheme';
export const darkTheme = createMuiTheme({
palette: {
type: 'dark', // Name of the theme
primary: {
main: '#152B38',
},
secondary: {
main: '#65C5C7',
},
contrastThreshold: 3,
tonalOffset: 0.2,
},
});
I defiend a themeProvider function and in this function I wrapped the material-ui's ThemeProvider in a React context to be able to change the theme easily:
import React, { useState } from 'react';
import {ThemeProvider} from "#material-ui/core/styles/";
import { lightTheme } from "./light";
import { darkTheme } from "./dark";
const getThemeByName = (theme: string) => {
return themeMap[theme];
}
const themeMap: { [key: string]: any } = {
lightTheme,
darkTheme
};
export const ThemeContext = React.createContext(getThemeByName('darkTheme'));
const ThemeProvider1: React.FC = (props) => {
// State to hold the selected theme name
const [themeName, _setThemeName] = useState('darkTheme');
// Retrieve the theme object by theme name
const theme = getThemeByName(themeName);
return (
<ThemeContext.Provider value={_setThemeName}>
<ThemeProvider theme={theme}>{props.children}</ThemeProvider>
</ThemeContext.Provider>
);
}
export default ThemeProvider1;
Now I can use it in my components like this:
import React from 'react';
import styled from 'styled-components';
import useTheme from "#material-ui/core/styles/useTheme";
const MyCardHeader = styled.div`
width: 100%;
height: 40px;
background-color: ${props => props.theme.bgColor};
color: ${props => props.theme.txtColor};
display: flex;
align-items:center;
justify-content: center;
`;
export default function CardHeader(props: { title: React.ReactNode; }) {
const theme = {
bgColor: useTheme().palette.primary.main,
txtColor: useTheme().palette.primary.contrastText
};
return (
<MyCardHeader theme={theme}>
{props.title}
</MyCardHeader>
);
}
For Changing between themes:
import React, {useContext} from 'react';
import { ThemeContext} from './themes/themeProvider';
export default function Header() {
// Get the setter function from context
const setThemeName = useContext(ThemeContext);
return (
<header>
<button onClick={() => setThemeName('lightTheme')}>
light
</button>
<button onClick={() => setThemeName('darkTheme')}>
dark
</button>
</header>
);
}
I'm using Material UI v4.
I tried something like Ashkan's answer, but it didn't work for me.
However, I found this in the documentation, and abstracting it to apply to a different piece of state, instead of user preference, worked for me. For your example, I'd probably make a context:
// context.js
import React, { useContext } from "react";
import { ThemeProvider, createTheme } from "#material-ui/core/styles";
import CssBaseline from "#material-ui/core/CssBaseline";
const CustomThemeContext = React.createContext();
// You can add more to these and move them to a separate file if you want.
const darkTheme = {
palette: {
type: "dark",
}
}
const lightTheme = {
palette: {
type: "light",
}
}
export function CustomThemeProvider({ children }) {
const [dark, setDark] = React.useState(false);
function toggleTheme() {
if (dark === true) {
setDark(false);
} else {
setDark(true);
}
}
const theme = React.useMemo(
() => {
if (dark === true) {
return createTheme(darkTheme);
}
return createTheme(lightTheme);
},
[dark],
);
return (
<CustomThemeContext.Provider value={toggleTheme}>
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
</CustomThemeContext.Provider>
);
}
export function useToggleTheme() {
const context = useContext(CustomThemeContext);
if (context === undefined) {
throw new Error("useCustomThemeContext must be used within an CustomThemeProvider");
}
return context;
}
Then wrap your app in that:
ReactDOM.render(
<CustomThemeProvider>
<App />
</CustomThemeProvider>,
document.getElementById("app")
);
And then access it in your app:
export default function App(){
const toggleTheme = useToggleTheme();
return (
<div>
<button onClick={toggleTheme}>Toggle the theme!!</button>
</div>
);
}
On a side note, in my app, I actually have a different theme in two sections of the app, based on whether the user is logged in or not. So I'm just doing this:
function App() {
const { authState } = useAuthContext();
const theme = React.useMemo(
() => {
if (authState.user) {
return createTheme(dashboardTheme);
}
return createTheme(loginTheme);
},
[authState.user],
);
return (
<ThemeProvider theme={theme}>
<TheRestOfTheApp />
</ThemeProvider>
}
It seems you can base the theme off any piece of state, or multiple pieces, by referencing them in useMemo and including them in the dependency array.
EDIT:
I just noticed that MUI v5 actually has something very similar in their docs.
In your code, theme type is changed. But the Page is not re-rendered with new theme.
I have changed code in index.js and App.js like following.
Try this approach. It works.
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(
<App/>,
document.getElementById("app")
);
App.js
import React from "react";
import CssBaseline from "#material-ui/core/CssBaseline";
import Typography from "#material-ui/core/Typography";
import { Button } from "#material-ui/core";
import { MuiThemeProvider, createMuiTheme } from "#material-ui/core/styles";
import blueGrey from "#material-ui/core/colors/blueGrey";
import lightGreen from "#material-ui/core/colors/lightGreen";
class App extends React.Component {
constructor(props){
super(props);
this.state = {
themeType : 'dark',
}
}
changeTheme(){
if (this.state.themeType == 'dark'){
this.setState({themeType:'light'});
} else {
this.setState({themeType:'dark'});
}
}
render() {
let theme = createMuiTheme({
palette: {
primary: {
light: lightGreen[300],
main: lightGreen[500],
dark: lightGreen[700]
},
secondary: {
light: blueGrey[300],
main: blueGrey[500],
dark: blueGrey[700]
},
type: this.state.themeType
}
});
return (
<MuiThemeProvider theme={theme}>
<CssBaseline />
<Typography>Hi there!</Typography>
<Button
variant="contained"
color="secondary"
onClick={()=>{this.changeTheme()}}
>
Change
</Button>
</MuiThemeProvider>
);
}
}
export default App;

Resources