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>
);
}
}