ReactHustle

How to Set Up MUI with NextJS, Emotion and Typescript

Jasser Mark Arioste

Jasser Mark Arioste

How to Set Up MUI with NextJS, Emotion and Typescript

Hello, hustlers! In this tutorial, you'll learn how to set up MaterialUI (MUI) with Emotion, and NextJS (SSR using the /pages directory), and Typescript. 

Introduction #

In many of my personal projects, I use the MaterialUI library to speed up front-end development time by using its wide array of react components. However, when I was just starting out, it was a bit tricky to set up to make sure that SSR works correctly, etc. In this tutorial, we'll learn how to exactly set this up.

What is MUI? #

MaterialUI (MUI) is one of the most popular component libraries in React. It exports production-ready, accessible, and customizable components that speed up development time. It also has a built-in theme system which makes it hard to go wrong when implementing a specific design system or theme.

What is NextJS? #

NextJS is a very popular, open-source react framework. It has a wide array of built-in features like built-in optimizations, data fetching, routing, client and server-side rendering, and many more. 

Using NextJS, MUI, and Typescript is a really good combination when building web applications that are not only maintainable but performant as well.

Tutorial Objectives #

  1. Set up a new NextJS v13 project with Typescript. We'll use the /pages directory for this tutorial. The /app directory has a slightly different setup that we'll cover in another tutorial.
  2. Installing dependencies like MUI, Emotion, etc
  3. Using MUI to render components
  4. Creating a custom theme and using a NextJS font

With everything clear, let's start. Note that some of these steps are taken from material-ui/examples repository.

Step 1: Creating a New NextJS (Typescript) Project #

Let's create a new NextJS project that includes Typescript. Run the following command:

npx create-next-app --ts nextjs-mui-typescript-starter

The --ts flag will create a new NextJS project with Typescript installed.

Step 2: Installing MUI and Emotion #

Next, let's install MUI into our project. When installing MUI you'll have a choice of what styling engine to use, styled-components or emotion. In this tutorial, we'll use emotion.

cd nextjs-mui-typecsript-starter
npm install @mui/material @emotion/react @emotion/styled @emotion/server
12

This command installs the MUI core components and the emotion styling engine.

Step 3: Creating the emotionCache factory #

First, we'll need to create an isomorphic emotion cache that works for both client and server. Create the file utils/createEmotionCache.ts:

// utils/createEmotionCache.ts
import createCache from "@emotion/cache";

const isBrowser = typeof document !== "undefined";

// On the client side, Create a meta tag at the top of the <head> and set it as insertionPoint.
// This assures that MUI styles are loaded first.
// It allows developers to easily override MUI styles with other styling solutions, like CSS modules.
export default function createEmotionCache() {
  let insertionPoint;

  if (isBrowser) {
    const emotionInsertionPoint = document.querySelector<HTMLMetaElement>(
      'meta[name="emotion-insertion-point"]'
    );
    insertionPoint = emotionInsertionPoint ?? undefined;
  }

  return createCache({ key: "mui-style", insertionPoint });
}
1234567891011121314151617181920

Step 4: Modifying the pages/_app.tsx file. #

Next, we'll need to modify the _app.tsx file so that it can render the <ThemeProvider/> and <CSSBaseline/> components.

// pages/_app.tsx
import Head from "next/head";
import { AppProps } from "next/app";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import { CacheProvider, EmotionCache } from "@emotion/react";
import createEmotionCache from "../utils/createEmotionCache";

// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();

// later we'll modify this to its own file
const theme = createTheme();
export interface MyAppProps extends AppProps {
  emotionCache?: EmotionCache;
}

export default function MyApp(props: MyAppProps) {
  // If there's no emotionCache rendered by the server, use the clientSideEmotionCache
  const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
  return (
    <CacheProvider value={emotionCache}>
      <Head>
        <meta name="viewport" content="initial-scale=1, width=device-width" />
      </Head>
      <ThemeProvider theme={theme}>
        {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
        <CssBaseline />
        <Component {...pageProps} />
      </ThemeProvider>
    </CacheProvider>
  );
}
123456789101112131415161718192021222324252627282930313233

Step 5: Creating a custom _document.tsx #

Next, we'll need to modify the _document.tsx  file so that SSR will work properly. If you haven't already, create the file pages/_document.tsx and copy the code below.  I've added comments in add

// pages/_document.tsx
import Document, {
  Html,
  Head,
  Main,
  NextScript,
  DocumentProps,
  DocumentContext,
} from "next/document";
import { AppType } from "next/app";
import { MyAppProps } from "./_app";
import createEmotionCache from "../utils/createEmotionCache";
import createEmotionServer from "@emotion/server/create-instance";

interface MyDocumentProps extends DocumentProps {
  emotionStyleTags: JSX.Element[];
}

export default function MyDocument({ emotionStyleTags }: MyDocumentProps) {
  return (
    <Html lang="en">
      <Head>
        <link rel="shortcut icon" href="/favicon.ico" />
        {/* Insertion point for client. This connects with createEmotionCache.ts */}
        <meta name="emotion-insertion-point" content="" />
        {emotionStyleTags}
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with static-site generation (SSG).
MyDocument.getInitialProps = async (ctx: DocumentContext) => {
  const originalRenderPage = ctx.renderPage;

  // You can consider sharing the same Emotion cache between all the SSR requests to speed up performance.
  // However, be aware that it can have global side effects.
  const cache = createEmotionCache();
  const { extractCriticalToChunks } = createEmotionServer(cache);

  // We're passing `emotionCache` to App component
  ctx.renderPage = () =>
    originalRenderPage({
      enhanceApp: (
        App: React.ComponentType<React.ComponentProps<AppType> & MyAppProps>
      ) =>
        function EnhanceApp(props) {
          return <App emotionCache={cache} {...props} />;
        },
    });

  const initialProps = await Document.getInitialProps(ctx);
  // This is important. It prevents Emotion to render invalid HTML.
  // See https://github.com/mui/material-ui/issues/26561#issuecomment-855286153
  const emotionStyles = extractCriticalToChunks(initialProps.html);
  const emotionStyleTags = emotionStyles.styles.map((style) => (
    <style
      data-emotion={`${style.key} ${style.ids.join(" ")}`}
      key={style.key}
      // eslint-disable-next-line react/no-danger
      dangerouslySetInnerHTML={{ __html: style.css }}
    />
  ));

  return {
    ...initialProps,
    // return emotionStyleTags as props
    emotionStyleTags,
  };
};
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475

Step 6: Using Modularized Imports #

To speed up development time and NextJS compilation time when using MUI, we have to use the modularizeImports option in the NextJS config.

If you're using a big package like @mui/icons-material, not using modularized imports could drastically slow down the NextJS compilation time. In the screenshot below, we're using @mui/icons-material and NextJS had to load 11717 modules and took 11.2s to do so. That's quite slow.

NextJS without modularizeImports config option

To fix this, let's modify our next.config.js file. Add the highlighted lines below to your config file:

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  modularizeImports: {
    "@mui/material": {
      transform: "@mui/material/{{member}}",
    },
    "@mui/icons-material": {
      transform: "@mui/icons-material/{{member}}",
    },
    "@mui/styles": {
      transform: "@mui/styles/{{member}}",
    },
    "@mui/lab": {
      transform: "@mui/lab/{{member}}",
    },
  },
};

module.exports = nextConfig;
123456789101112131415161718192021

After this step, restart your server using yarn dev. We can see that in the screenshot below, NextJS only compiled 700 modules and took 1.27s.

NextJS with modularizeImports config option

To learn more about this option, you can read its documentation here.

With this, we should be done with the setup. All that's left is to test if everything works properly.

Step 7: Rendering MUI Components #

Next, let's render some components inside pages/index.tsx:

// pages/index.tsx
import type { NextPage } from "next";

import { Typography } from "@mui/material";

const Home: NextPage = () => {
  return (
    <div>
      <Typography variant="h2" textAlign="center">
        Hello, World
      </Typography>
    </div>
  );
};

export default Home;

12345678910111213141516

See the output below, everything is working correctly:

NextJS MUI Typescript Output

Step 8: Checking the Inserted Styles #

Next, let's check if the styles are inserted in the HTML document when the page is server-side rendered. Go to view-source:http://localhost:3000/. and check the "line wrap" option.

NextJS MUI Inserted Style Tags

We can see that styles are inserted after the emotion-insertion-point <meta/> tag.

Step 9: Creating a Custom Theme with NextJS font #

Currently, our theme is using the Roboto font. But let's modify it so that it uses the Inter font. First, create the file /src/themes/defaultTheme.tsx and copy the code below. We also modified some colors for this theme.

// src/themes/defaultTheme.tsx
import { Inter } from "next/font/google";
import { createTheme } from "@mui/material/styles";
import { red } from "@mui/material/colors";

export const inter = Inter({
  weight: ["300", "400", "500", "700"],
  subsets: ["latin"],
  display: "swap",
  fallback: ["Helvetica", "Arial", "sans-serif"],
});

// Create a theme instance.
const defaultTheme = createTheme({
  palette: {
    primary: {
      main: "#556cd6",
    },
    secondary: {
      main: "#19857b",
    },
    error: {
      main: red.A400,
    },
  },
  typography: {
    fontFamily: inter.style.fontFamily,
  },
});

export default defaultTheme;

12345678910111213141516171819202122232425262728293031

Next, let us modify pages/_app.tsx to use our default theme.

// pages/_app.tsx
import defaultTheme from "../src/themes/defaultTheme";
// ... omitted for brevity
export default function MyApp(props: MyAppProps) {
  // If there's no emotionCache rendered by the server, use the clientSideEmotionCache
  const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
  return (
    <CacheProvider value={emotionCache}>
      <Head>
        <meta name="viewport" content="initial-scale=1, width=device-width" />
      </Head>
      <ThemeProvider theme={defaultTheme}>
        {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
        <CssBaseline />
        <Component {...pageProps} />
      </ThemeProvider>
    </CacheProvider>
  );
}
12345678910111213141516171819

Next, let's modify pages/_document.tsx and add theme color for PWA using our default theme.

// pages/_document.tsx
import Document, {
  Html,
  Head,
  Main,
  NextScript,
  DocumentProps,
  DocumentContext,
} from "next/document";
import { AppType } from "next/app";
import { MyAppProps } from "./_app";
import createEmotionCache from "../utils/createEmotionCache";
import createEmotionServer from "@emotion/server/create-instance";
import defaultTheme from "../src/themes/defaultTheme";

interface MyDocumentProps extends DocumentProps {
  emotionStyleTags: JSX.Element[];
}

export default function MyDocument({ emotionStyleTags }: MyDocumentProps) {
  return (
    <Html lang="en">
      <Head>
        <link rel="shortcut icon" href="/favicon.ico" />
        {/* PWA primary color */}
        <meta name="theme-color" content={defaultTheme.palette.primary.main} />
        {/* Insertion point for client. This connects with createEmotionCache.ts */}
        <meta name="emotion-insertion-point" content="" />
        {emotionStyleTags}
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}
12345678910111213141516171819202122232425262728293031323334353637

That's pretty much it!

Full Code and Demo #

The full code is available at GitHub: jmarioste/nextjs-mui-typescript-starter

You can check out the demo at Stackblitz: NextJS MUI Typescript Starter

Conclusion #

You learned how to set up MUI with NextJS and Typescript inside the /pages directory. We also created a custom theme with a NextJS font to satisfy our project requirements.

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on Twitter.

Resources #

Credits: Image by Lukas Bieri from Pixabay

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle