⛩️

Next.js(app router) ✖️ MUIv5でテンプレートレイアウトを作ってみた!

2024/01/24に公開

はじめに

個人開発で、毎回Headerやfooterやナビゲージョンなど作っていたのですが、久々に作るとなると色々と面倒だったので、
自分で簡単なテンプレートを作りました。カスタマイズして使って頂けると嬉しいです!
headerやfooter、ログイン、新規登録画面を作りました。





Next.jsをセットアップ

npx create-next-app@latest

基本的に全部yesで大丈夫です!

MUIのライブラリをインストール

npm install @mui/material @emotion/react @emotion/styled

ディレクトリ設計

srcディレクトリ中身は下記のようにしています。

├── app
│   ├── (auth)
│   │   ├── signin
│   │   │   └── page.tsx
│   │   └── signup
│   │       └── page.tsx
│   ├── layout.tsx
│   └── page.tsx
├── components
│   ├── form
│   │   └── auth
│   │       ├── SignInForm.tsx
│   │       └── SignUpForm.tsx
│   ├── layouts
│   │   ├── ApplicationContainer.tsx
│   │   ├── Footer.tsx
│   │   └── Header.tsx
│   └── templates
│       └── auth
│           ├── SignInTemplate.tsx
│           └── SignUpTemplate.tsx
└── styles
    └── globals.css

レイアウトのソースコードサンプル

src/components/layouts/Header.tsx
'use client';

import React, { useState } from 'react';
import MenuIcon from '@mui/icons-material/Menu';
import {
  AppBar,
  IconButton,
  Typography,
  Container,
  Box,
  List,
  ListItem,
  Divider,
  Button,
  CssBaseline,
  Drawer,
} from '@mui/material';
import Link from 'next/link';

interface Props {
  window?: () => Window;
}
export const Header = (props: Props) => {
  const drawerWidth = 240;
  const { window } = props;
  const [mobileOpen, setMobileOpen] = useState(false);

  const handleDrawerToggle = () => {
    setMobileOpen((prevState) => {
      return !prevState;
    });
  };

  const container =
    window !== undefined
      ? () => {
          return window().document.body;
        }
      : undefined;

  return (
    <AppBar color="success" component="header" position="fixed">
      <Container>
        <Box
          sx={{
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            height: '80px',
          }}
        >
          <CssBaseline />
          <Box>
            <Typography variant="h5" component="div" sx={{ flexGrow: 1 }}>
              <Link href={'/'}>Header</Link>
            </Typography>
          </Box>
          <Box>
            <IconButton
              color="inherit"
              aria-label="open drawer"
              edge="start"
              onClick={handleDrawerToggle}
              sx={{ display: { sm: 'none' } }}
            >
              <MenuIcon fontSize="large" />
            </IconButton>
          </Box>
          <Box sx={{ display: { xs: 'none', sm: 'block' } }}>
            <Button sx={{ color: '#fff' }}>
              <Link href={'/'}>Home</Link>
            </Button>
            <Button sx={{ color: '#fff' }}>
              <Link href={'/signin'}>SignIn</Link>
            </Button>
            <Button sx={{ color: '#fff' }}>
              <Link href={'/signup'}>SignUp</Link>
            </Button>
          </Box>

          <Drawer
            container={container}
            variant="temporary"
            open={mobileOpen}
            onClose={handleDrawerToggle}
            ModalProps={{
              keepMounted: true,
            }}
            anchor="right"
            sx={{
              display: { xs: 'block', sm: 'none' },
              '& .MuiDrawer-paper': {
                boxSizing: 'border-box',
                width: drawerWidth,
              },
            }}
          >
            <Box onClick={handleDrawerToggle} sx={{ textAlign: 'center' }}>
              <Typography variant="h6" sx={{ my: 2 }}>
                MUI
              </Typography>
              <Divider />
              <List>
                <ListItem>
                  <Link href={'/'}>Home</Link>
                </ListItem>
                <ListItem>
                  <Link href={'/signin'}>SignIn</Link>
                </ListItem>
                <ListItem>
                  <Link href={'/signup'}>SignUp</Link>
                </ListItem>
              </List>
            </Box>
          </Drawer>
        </Box>
      </Container>
    </AppBar>
  );
};

src/components/layouts/Footer.tsx
'use client';

import React from 'react';

import { AppBar, Typography, Container, Box, CssBaseline } from '@mui/material';

export const Footer = () => {
  return (
    <AppBar
      color="primary"
      component="footer"
      position="static"
      sx={{ marginTop: 'auto' }}
    >
      <Container>
        <Box
          sx={{
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            height: '80px',
            marginTop: 'auto',
          }}
        >
          <CssBaseline />
          <Box>
            <Typography variant="h5" component="div" sx={{ flexGrow: 1 }}>
              Footer
            </Typography>
          </Box>
        </Box>
      </Container>
    </AppBar>
  );
};

ApplicationContainer (共通化コンポーネント)

ApplicationContainerコンポーネントはHeaderとFooterを入れて共通化して使っています。
style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}でfooterを最下部に固定しています。

src/components/layouts/ApplicationContainer.tsx
import React, { FC } from 'react';
import { Footer } from '@/components/layouts/Footer';
import { Header } from '@/components/layouts/Header';

type Props = {
  children: React.ReactNode;
};
export const ApplicationContainer: FC<Props> = ({ children }) => {
  return (
    <div
      style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}
    >
      <Header />
      <main>{children}</main>
      <Footer />
    </div>
  );
};


フォームのソースコードサンプル

ログイン

src/components/form/auth/SignInForm.tsx
'use client';

import React from 'react';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Checkbox from '@mui/material/Checkbox';
import Container from '@mui/material/Container';
import CssBaseline from '@mui/material/CssBaseline';
import FormControlLabel from '@mui/material/FormControlLabel';
import Grid from '@mui/material/Grid';
import Link from '@mui/material/Link';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';

// TODO remove, this demo shouldn't need to reset the theme.
const defaultTheme = createTheme();

export const SignInForm = () => {
  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    console.log({
      email: data.get('email'),
      password: data.get('password'),
    });
  };

  return (
    <ThemeProvider theme={defaultTheme}>
      <Container component="main" maxWidth="xs" sx={{ marginTop: 16 }}>
        <CssBaseline />
        <Box
          sx={{
            marginTop: 8,
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
          }}
        >
          <Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
            <LockOutlinedIcon />
          </Avatar>
          <Typography component="h1" variant="h5">
            Sign in
          </Typography>
          <Box
            component="form"
            onSubmit={handleSubmit}
            noValidate
            sx={{ mt: 1 }}
          >
            <TextField
              margin="normal"
              required
              fullWidth
              id="email"
              label="Email Address"
              name="email"
              autoComplete="email"
              autoFocus
            />
            <TextField
              margin="normal"
              required
              fullWidth
              name="password"
              label="Password"
              type="password"
              id="password"
              autoComplete="current-password"
            />
            <FormControlLabel
              control={<Checkbox value="remember" color="primary" />}
              label="Remember me"
            />
            <Button
              type="submit"
              fullWidth
              variant="contained"
              sx={{ mt: 3, mb: 2 }}
            >
              Sign In
            </Button>
            <Grid container>
              <Grid item xs>
                <Link href="#" variant="body2">
                  Forgot password?
                </Link>
              </Grid>
              <Grid item>
                <Link href="/signup" variant="body2">
                  {"Don't have an account? Sign Up"}
                </Link>
              </Grid>
            </Grid>
          </Box>
        </Box>
      </Container>
    </ThemeProvider>
  );
};

新規登録

src/components/form/auth/SignUpForm.tsx
'use client';

import React from 'react';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Checkbox from '@mui/material/Checkbox';
import Container from '@mui/material/Container';
import CssBaseline from '@mui/material/CssBaseline';
import FormControlLabel from '@mui/material/FormControlLabel';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';

// TODO remove, this demo shouldn't need to reset the theme.
const defaultTheme = createTheme();

export const SignUpForm = () => {
  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    console.log({
      email: data.get('email'),
      password: data.get('password'),
    });
  };

  return (
    <ThemeProvider theme={defaultTheme}>
      <Container component="main" maxWidth="xs" sx={{ marginTop: 16 }}>
        <CssBaseline />
        <Box
          sx={{
            marginTop: 8,
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
          }}
        >
          <Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
            <LockOutlinedIcon />
          </Avatar>
          <Typography component="h1" variant="h5">
            Sign Up
          </Typography>
          <Box
            component="form"
            onSubmit={handleSubmit}
            noValidate
            sx={{ mt: 1 }}
          >
            <TextField
              margin="normal"
              required
              fullWidth
              id="email"
              label="Email Address"
              name="email"
              autoComplete="email"
              autoFocus
            />
            <TextField
              margin="normal"
              required
              fullWidth
              name="password"
              label="Password"
              type="password"
              id="password"
              autoComplete="current-password"
            />
            <TextField
              margin="normal"
              required
              fullWidth
              name="passwordConfirm"
              label="PasswordConfirm"
              type="password"
              id="passwordConfirm"
              autoComplete="current-password"
            />
            <FormControlLabel
              control={<Checkbox value="remember" color="primary" />}
              label="Remember me"
            />
            <Button
              type="submit"
              fullWidth
              variant="contained"
              sx={{ mt: 3, mb: 2 }}
            >
              Sign Up
            </Button>
          </Box>
        </Box>
      </Container>
    </ThemeProvider>
  );
};

template

templateファイルに必要なレイアウトを全部入れて、page.tsxに入れます!

ログイン

src/components/templates/auth/SignInTemplate.tsx
import React from 'react';
import { SignInForm } from '@/components/form/auth/SignInForm';
import { ApplicationContainer } from '@/components/layouts/ApplicationContainer';

export const SignInTemplate = () => {
  return (
    <ApplicationContainer>
      <SignInForm />
    </ApplicationContainer>
  );
};

新規登録

src/components/templates/auth/SignUpTemplate.tsx
import React from 'react';
import { SignUpForm } from '@/components/form/auth/SignUpForm';
import { ApplicationContainer } from '@/components/layouts/ApplicationContainer';

export const SignUpTemplate = () => {
  return (
    <ApplicationContainer>
      <SignUpForm />
    </ApplicationContainer>
  );
};

page(ルーティングページ)

ログイン

src/app/(auth)/signin/page.tsx
import React from 'react';
import { SignInTemplate } from '@/components/templates/auth/SignInTemplate';

const SignInPage = () => {
  return <SignInTemplate />;
};

export default SignInPage;

新規登録

src/app/(auth)/signup/page.tsx
import React from 'react';
import { SignUpTemplate } from '@/components/templates/auth/SignUpTemplate';

const SignUpPage = () => {
  return <SignUpTemplate />;
};

export default SignUpPage;

RootLayout

src/app/layout.tsxにはApp RouterでMUIが使えるようにセットアップをします。
https://mui.com/material-ui/guides/nextjs/

src/app/layout.tsx
import React from 'react';
import { AppRouterCacheProvider } from '@mui/material-nextjs/v13-appRouter';
import { Inter } from 'next/font/google';
import type { Metadata } from 'next';
import '@/styles/globals.css';

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
};

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
});

const RootLayout = ({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) => {
  return (
    <html lang="ja" className={inter.className}>
      <body>
        <AppRouterCacheProvider>{children}</AppRouterCacheProvider>
      </body>
    </html>
  );
};

export default RootLayout;

トップページ

試しにトップページにサンプルのテンプレートを作ってみました。

src/app/page.tsx
import {
  Box,
  Button,
  Card,
  CardActions,
  CardContent,
  CardMedia,
  Container,
  Grid,
  Stack,
  Typography,
} from '@mui/material';
import { ApplicationContainer } from '@/components/layouts/ApplicationContainer';

const Home = () => {
  const cards = [1, 2, 3, 4, 5, 6, 7, 8, 9];

  return (
    <ApplicationContainer>
      <Box
        sx={{
          bgcolor: 'background.paper',
          pt: 10,
          pb: 6,
        }}
      >
        <Container maxWidth="md" sx={{ mt: 10 }}>
          <Typography
            component="h1"
            variant="h2"
            align="center"
            color="text.primary"
            gutterBottom
          >
            Album layout
          </Typography>
          <Typography
            variant="h5"
            align="center"
            color="text.secondary"
            paragraph
          >
            Something short and leading about the collection below—its contents,
            the creator, etc. Make it short and sweet, but not too short so
            folks don&apos;t simply skip over it entirely.
          </Typography>
          <Stack
            sx={{ pt: 4 }}
            direction="row"
            spacing={2}
            justifyContent="center"
          >
            <Button variant="contained">Main call to action</Button>
            <Button variant="outlined">Secondary action</Button>
          </Stack>
        </Container>
      </Box>
      <Container sx={{ py: 8 }} maxWidth="md">
        {/* End hero unit */}
        <Grid container spacing={4}>
          {cards.map((card) => {
            return (
              <Grid item key={card} xs={12} sm={6} md={4}>
                <Card
                  sx={{
                    height: '100%',
                    display: 'flex',
                    flexDirection: 'column',
                  }}
                >
                  <CardMedia
                    component="div"
                    sx={{
                      // 16:9
                      pt: '56.25%',
                    }}
                    image="https://source.unsplash.com/random?wallpapers"
                  />
                  <CardContent sx={{ flexGrow: 1 }}>
                    <Typography gutterBottom variant="h5" component="h2">
                      Heading
                    </Typography>
                    <Typography>
                      This is a media card. You can use this section to describe
                      the content.
                    </Typography>
                  </CardContent>
                  <CardActions>
                    <Button size="small">View</Button>
                    <Button size="small">Edit</Button>
                  </CardActions>
                </Card>
              </Grid>
            );
          })}
        </Grid>
      </Container>
    </ApplicationContainer>
  );
};

export default Home;

Discussion