About

Start a project using Next.js and MUI

Jul 11th, 2022

5 min read

Step 1 : Create Next.js project (สำหรับ typescript projectสามารถเพิ่ม flag --typescript ในคำสั่งได้เลยค่ะ)
⁠npx create-next-app@latest --typescript

เมื่อ run คำสั่งนี้เสร็จแล้ว ลอง run project ด้วยคำสั่ง npm run dev

Step 2 : Install MUI
⁠npm install @mui/material @emotion/react @emotion/styled

Step 3 : ในกรณีที่เราใช้ MUI กับ Next.js เราต้อง install emotion package เพิ่มเติม
⁠npm i @emotion/cache @emotion/react @emotion/server @emotion/styled

Step 4 : สร้าง MUI theme
โดยการสร้าง folder 'theme' ใน styles folderจากนั้นสร้างไฟล์ index.ts, pallette.ts, และ typography.js

create-mui-theme-ts.png

index.ts เป็นไฟล์สำหรับแก้ไข MUI theme ไฟล์หลัก

import { createTheme, Theme as _Theme } from "@mui/material/styles";
import { createBreakpoints } from "@mui/system";
import palette from "./palette";
import typography from "./typography";

interface Theme extends _Theme {
    status: any;
}

const breakpoints = createBreakpoints({});
// Create a theme instance.
export const theme: Theme = createTheme({
    status: {
        appState: "loading",
    },
    typography,
    palette,
    shape: {
        borderRadius: 6,
    },
    spacing: 8,
    breakpoints: {
        values: {
            xs: 0,
            sm: 576,
            md: 768,
            lg: 992,
            xl: 1200,
        },
    },
  
}) as any;

export default theme;

pallete.ts เป็นไฟล์ที่ใช้จัดการธีมสีต่างๆ เช่น Primary, Secondary, Success


import { colors } from '@mui/material';

const white = '#FFFFFF';
const black = '#222';

export default {
  primary: {
    contrastText: white,
    dark: black,
    main: black,
    light: '#212121',

  },
  secondary: {
    contrastText: white,
    dark: '#111827',
    main: '#1f2937',
    light: '#4b5563',
  },
  success: {
    contrastText: white,
    dark: "#54cb7b",
    main: "#4EC274",
    light: colors.green[400],
  },
  info: {
    contrastText: white,
    dark: '#737373',
    main: '#949494',
    light: '#949494',
  },
  warning: {
    contrastText: white,
    dark: colors.orange[900],
    main: colors.orange[600],
    light: colors.orange[400],
  },
  error: {
    contrastText: white,
    dark: '#960010',
    main: '#F72220',
    light: '#ff6d61',
  },
  text: {
    primary: black,
    secondary: '#949494',
    link: black,
  },
  background: {
    default: '#f9f9f9', 
    paper: white,
  },
  icon: colors.blueGrey[600],
  divider: '#E0E0E0',
  border: '#cecece',
};

typography.ts เป็นไฟล์ที่ใช้จัดการเกี่ยวกับ ตัวหนังสือ ซึ่ง reference จาก variant ของ Component typography ของ MUI เช่น h1, h2, body1, subtitle1


import { createBreakpoints } from '@mui/system';
import palette from './palette';

const breakpoints = createBreakpoints({});
export default {
  fontFamily: 'Nunito, sans-serif',
  h1: {
    color: palette.text.primary,
    fontWeight: 700,
    fontSize: '32px',
    lineHeight: '1.2',
    [breakpoints.up('sm')]: {
      fontSize: '42px',
    },


  },
  h2: {
    color: palette.text.primary,
    fontWeight: 700,
    fontSize: '26px',
    letterSpacing: '0',
    lineHeight: '32px',
    marginBottom: '20px',
  },
  h3: {
    color: palette.text.primary,
    fontWeight: 600,
    fontSize: '22px',
    letterSpacing: '0',
    lineHeight: '28px',
  },
  h4: {
    color: palette.text.primary,
    fontWeight: 700,
    fontSize: '18px',
    letterSpacing: '0',
    lineHeight: '24px',
  },
  h5: {
    color: palette.text.primary,
    fontWeight: 500,
    fontSize: '16px',
    letterSpacing: '-0.05px',
    lineHeight: '20px',
  },
  h6: {
    color: palette.text.primary,
    fontWeight: 500,
    fontSize: '14px',
    letterSpacing: '-0.05px',
    lineHeight: '20px',
  },
  subtitle1: {
    color: palette.text.primary,
    fontSize: '14px',
    fontWeight: '500',
    letterSpacing: '0',
    lineHeight: '20px',

    [breakpoints.up('sm')]: {
      fontSize: '18px',
    },

  },
  subtitle2: {
    color: palette.black,
    fontSize: '16px',
    fontWeight: '600',
    letterSpacing: '0',
    lineHeight: '20px',

  },
  body1: {
    color: palette.text.primary,
    fontSize: '16px',
    letterSpacing: '0',
    lineHeight: '20px',

  },
  body2: {
    color: palette.black,
    fontSize: '18px',
    letterSpacing: '0',
    lineHeight: '22px',
    // whiteSpace: 'pre-line',
    '&.MuiTypography-gutterBottom': {
      marginBottom: '16px',
    }
  },
  button: {
    color: palette.text.primary,
    fontSize: '16px',

    fontWeight: '400',
  },
  caption: {
    color: palette.text.secondary,
    fontSize: '11px',
    letterSpacing: '0.33px',
    lineHeight: '13px',
  },
  overline: {
    color: palette.text.secondary,
    fontSize: '11px',
    fontWeight: 500,
    letterSpacing: '0.33px',
    lineHeight: '13px',
    textTransform: 'uppercase',
  },
  a: {
    color: palette.text.primary,
  },
  label: {
    fontSize: '16px',
    fontWeight: 700,
    color: palette.text.primary,
  },
};

Step 5 : Create an emotion cache utils/createEmotionCache.ts
ไฟล์ emotionCache นี้จะช่วยให้เราสามารถ override MUI styles ได้ เช่น ในกรณีที่เราต้องเขียน CSS Modules

import createCache from '@emotion/cache';

const createEmotionCache = () => {
  return createCache({ key: 'css', prepend: true });
};

export default createEmotionCache;

Step 6 : Edit _app.tsx

import { CacheProvider, EmotionCache } from "@emotion/react";
import CssBaseline from "@mui/material/CssBaseline";
import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles";
import { AppProps } from "next/app";
import Head from "next/head";
import ModalProvider from "../src/provider/ModalProvider";
import createEmotionCache from "../src/styles/createEmotionCache";
import "../src/styles/globals.scss";
import theme from "../src/styles/theme";

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

interface MyAppProps extends AppProps {
    emotionCache?: EmotionCache;
}

export default function MyApp(props: MyAppProps) {
    const {
        Component,
        emotionCache = clientSideEmotionCache,
        pageProps,
    } = props;
    return (
        <CacheProvider value={emotionCache}>
            <Head>
                <meta
                    name="viewport"
                    content="initial-scale=1, width=device-width"
                />
            </Head>
            <StyledEngineProvider injectFirst>
                <ThemeProvider theme={theme}>
                    {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
                    <CssBaseline />
                    <ModalProvider>
                        <Component {...pageProps} />
                    </ModalProvider>
                </ThemeProvider>
            </StyledEngineProvider>
        </CacheProvider>
    );
}

ในไฟล์นี้เราจะเพิ่ม CacheProvider เข้าไป
เพิ่ม ThemeProvider เพื่อเรียกใช้ theme MUI ที่เรา custom ไว้
เรียกใช้ CssBaseline

ซึ่งหน้าที่ของ CssBaseline ก็จะเหมือนกับการใช้ normalize.css ซึ่งเป็นตัวช่วยปรับ style ที่บราวเซอร์แต่ละบราวเซอร์แสดงผลไม่เหมือนกัน ให้มีความสอดคล้องกัน และแสดงผลคล้ายกันมากขึ้น

Step 7 : Create custom _document.tsx

import createEmotionServer from "@emotion/server/create-instance";
import Document, { Head, Html, Main, NextScript } from "next/document";
import createEmotionCache from "../src/styles/createEmotionCache";
import theme from "../src/styles/theme";

export default class MyDocument extends Document {
    render() {
        return (
            <Html lang="en">
                <Head>
                    {/* PWA primary color */}
                    <meta
                        name="theme-color"
                        content={theme.palette.primary.main}
                    />
                    <link rel="shortcut icon" href="/static/favicon.ico" />

                    {/* Inject MUI styles first to match with the prepend: true configuration. */}
                    {(this.props as any).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) => {
    // Resolution order
    //
    // On the server:
    // 1. app.getInitialProps
    // 2. page.getInitialProps
    // 3. document.getInitialProps
    // 4. app.render
    // 5. page.render
    // 6. document.render
    //
    // On the server with error:
    // 1. document.getInitialProps
    // 2. app.render
    // 3. page.render
    // 4. document.render
    //
    // On the client
    // 1. app.getInitialProps
    // 2. page.getInitialProps
    // 3. app.render
    // 4. page.render

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

    ctx.renderPage = () =>
        originalRenderPage({
            enhanceApp: (App: any) =>
                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,
        emotionStyleTags,
    };
};

Reference


Related Posts