⛩️
Next.js(app router) ✖️ MUIv5でテンプレートレイアウトを作ってみた!
はじめに
個人開発で、毎回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
レイアウトのソースコードサンプル
Header
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>
);
};
Footer
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が使えるようにセットアップをします。
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'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