Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement SSR for Material UI's media queries in NextJs?

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>
    )   
  }
}
like image 902
Arturo Cuya Avatar asked Apr 29 '20 20:04

Arturo Cuya


People also ask

How does SSR work in Nextjs?

Next Js is a React-based framework that provides a developer with everything required for a production-grade application. SSR or Server Side Rendering is also known as dynamic rendering. In SSR the page is generated each time the server gets a request.

How do I apply media queries in material UI?

To use the useMediaQuery hook, first import it from Material-UI. import { useMediaQuery } from '@material-ui/core'; In the component, call the useMediaQuery hook and pass in a media query as the argument. This will return a true or false value.

Can I use media queries in react?

Generally, one can not do inline styling with media queries because React doesn't allow us to use media queries in inline styling. We can use radium which is a third-party package that enables media queries for inline styling.


1 Answers

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>
    );
  }
}
like image 67
Ryan Cogswell Avatar answered Oct 17 '22 16:10

Ryan Cogswell