Next.js + MUI で簡単な管理画面テンプレートを作ってみる(その1)
Next.jsとMaterial UI(以後MUI)を使用して、よくある管理画面テンプレートを使わずに、それなりに見える管理画面を作る方法です。
世の中にはたくさんの管理画面テンプレートがあります。検索すると無料のものから有料のものまで色々あり、どれも見栄えが良く高機能そうです。しかしそれらは以下のような問題があります。
- 高機能すぎて中で何をやっているのかよくわからない
- たくさんのテンプレートが生まれては消えていき、選んだテンプレートがいつまでメンテされるのかわからない
だったら、シンプルでいいので中の動きを理解でき、それなりに見栄えの良いテンプレートを自分で作っちゃおうという内容になります。
前提知識:Next.jsの基本的な知識
Material UI
Materialデザインを使用した見栄えの良いコンポーネント集です。サイトを覗くとコンポーネントの表示例とサンプルコードが以下のように参照できます。
部品のイメージとサンプルコードを確認できる
なのでサンプルコードをコピーしてペタペタ貼っていくと、コツを掴めばCSSやデザインの知識をあまり持っていなくてもそれなりの画面を作ることができます。
開発環境を作る
create-next-app
create-next-appを実行してソースコードの雛形を作ります。ここでは admin-ui
という名前で作ることにします。
$ npx create-next-app@latest admin-ui
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
完了後、admin-uiフォルダに移動してnpm run dev
で以下のような画面が出たらOKです。
初期画面
初期画面とスタイルをクリアする
src/app/globals.css
の中身をバッサリと消して空にします。続いてpage.tsxの中身もHello worldだけに書き換えます。
import Image from "next/image";
import styles from "./page.module.css";
export default function Home() {
return <div>Hello world</div>;
}
以下のような画面に変わります。
Material UIを入れる
パッケージを追加する
以下のコマンドを実行し、MUIをインストールします。
npm i @mui/material @emotion/react @emotion/styled @mui/material-nextjs @emotion/cache
*インストール後は npm run dev
の再実行が必要です。
layoutを書き換える
{children}
をAppRouterCacheProvider
タグで囲ってください。MUIを使うためのおまじないだと思ってください。
import type { Metadata } from "next";
import { AppRouterCacheProvider } from "@mui/material-nextjs/v14-appRouter";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<AppRouterCacheProvider>{children}</AppRouterCacheProvider>
</body>
</html>
);
}
これでMUIを使えるようになったはずです。では先ほどのpage.tsxを書き換えてみましょう。
以下のMUIのページを見ると色々な部品のサンプルコードがあるので適当に並べてみます。
Paper, Checkbox, Buttonのサンプルをコピーして並べてみました。
import styles from "./page.module.css";
import {
Button,
Checkbox,
Container,
FormControlLabel,
FormGroup,
Paper,
} from "@mui/material";
export default function Home() {
return (
<Container>
<Paper elevation={3} sx={{ p: 5 }}>
<h1 className={styles.title}>Welcome to Material UI!</h1>
<FormGroup>
<FormControlLabel
control={<Checkbox defaultChecked />}
label="Label"
/>
<FormControlLabel required control={<Checkbox />} label="Required" />
<FormControlLabel disabled control={<Checkbox />} label="Disabled" />
</FormGroup>
<hr />
<Button variant="contained" color="primary">
ボタン
</Button>
</Paper>
</Container>
);
}
それっぽい画面が簡単にできました。
MUI化成功
管理画面を作る
MUIのDrawerというコンポーネントを使って管理画面のレイアウトを作ります。
Drawerコンポーネント
このサンプルのトップのナビゲーションバーと左のサイドバーをレイアウトに設定し、右側のコンテンツ部分に各ページを表示するようにします。
上記MUIのサイトでshow code
ボタンを押してコードを表示し、layout.tsxに埋め込みます。
layout.tsxを編集する
準備
アイコンを表示するために、以下を参考に追加のパッケージをインストールします。
npm install @mui/icons-material
これでアイコンが表示できるようになりました。ではlayout.tsxにDrawerのサンプルコードを埋め込みましょう。
コード編集
以下のようなコードになります。
"use client";
import type { Metadata } from "next";
import { AppRouterCacheProvider } from "@mui/material-nextjs/v14-appRouter";
import { Inter } from "next/font/google";
import "./globals.css";
import {
Box,
CssBaseline,
AppBar,
Toolbar,
Typography,
Drawer,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Divider,
} from "@mui/material";
import InboxIcon from "@mui/icons-material/MoveToInbox";
import MailIcon from "@mui/icons-material/Mail";
const inter = Inter({ subsets: ["latin"] });
const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
const drawerWidth = 240;
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<AppRouterCacheProvider>
<Box sx={{ display: "flex" }}>
<CssBaseline />
<AppBar
position="fixed"
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
>
<Toolbar>
<Typography variant="h6" noWrap component="div">
Clipped drawer
</Typography>
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
sx={{
width: drawerWidth,
flexShrink: 0,
[`& .MuiDrawer-paper`]: {
width: drawerWidth,
boxSizing: "border-box",
},
}}
>
<Toolbar />
<Box sx={{ overflow: "auto" }}>
<List>
{["Inbox", "Starred", "Send email", "Drafts"].map(
(text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemIcon>
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>
</ListItem>
)
)}
</List>
<Divider />
<List>
{["All mail", "Trash", "Spam"].map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemIcon>
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>
</ListItem>
))}
</List>
</Box>
</Drawer>
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
<Toolbar />
{children}
</Box>
</Box>
</AppRouterCacheProvider>
</body>
</html>
);
}
ポイントは以下です。
- サンプルコードのTSX部分は
<AppRouterCacheProvider>
タグの内側に入れる - コンテンツ部分(
<Typography paragraph>
の部分)は削除し、{children}
に変える(この部分がpage.tsx
の内容に置き換わる) - zIndexを指定している部分がクライアント側でしか動かないと怒られるので
"use client";
を入れる(後で対処しますが一旦これに変えます) -
"use client";
を入れるとconst metadata
の部分でエラーが出るのでexportを消す(後で対処しますが一旦これに変えます)
ブラウザで確認すると、以下のように表示されます。
layout.tsxへの埋め込み後
どうですか?
それっぽい画面が表示されました。
ページを追加してみる
src/app/page1/page.tsx
というファイルを作り、以下の内容にします。
import React from "react";
const PageOne = () => {
return <h1>Page1</h1>;
};
export default PageOne;
http://localhost:3000/page1
にアクセスしてみましょう。新しいページがコンテンツ部分に表示されます。
新しいページの表示
layout.tsxを整理する
layout.tsxゴチャゴチャしているので整理します。ナビゲーションバー部分はnavigationbar.tsx
、サイドバー部分はsidebar.tsx
に切り出します。ついでにlayout.tsxの"use client";
も削除します。
src/app/_components
というフォルダを作り、そこにnavigationbar.tsx
とsidebar.tsx
を作りましょう。各ファイルは以下のようになります。
"use client";
import { AppBar, Toolbar, Typography } from "@mui/material";
import React from "react";
const NavigationBar = () => {
return (
<AppBar
position="fixed"
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
>
<Toolbar>
<Typography variant="h6" noWrap component="div">
Clipped drawer
</Typography>
</Toolbar>
</AppBar>
);
};
export default NavigationBar;
import {
Box,
Divider,
Drawer,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Toolbar,
} from "@mui/material";
import InboxIcon from "@mui/icons-material/MoveToInbox";
import MailIcon from "@mui/icons-material/Mail";
import React from "react";
const drawerWidth = 240;
const SideBar = () => {
return (
<Drawer
variant="permanent"
sx={{
width: drawerWidth,
flexShrink: 0,
[`& .MuiDrawer-paper`]: {
width: drawerWidth,
boxSizing: "border-box",
},
}}
>
<Toolbar />
<Box sx={{ overflow: "auto" }}>
<List>
{["Inbox", "Starred", "Send email", "Drafts"].map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemIcon>
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>
</ListItem>
))}
</List>
<Divider />
<List>
{["All mail", "Trash", "Spam"].map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemIcon>
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>
</ListItem>
))}
</List>
</Box>
</Drawer>
);
};
export default SideBar;
import type { Metadata } from "next";
import { AppRouterCacheProvider } from "@mui/material-nextjs/v14-appRouter";
import { Inter } from "next/font/google";
import "./globals.css";
import { Box, CssBaseline, Toolbar } from "@mui/material";
import NavigationBar from "./_components/navigationbar";
import SideBar from "./_components/sidebar";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Material-UI Admin Page Example",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<AppRouterCacheProvider>
<Box sx={{ display: "flex" }}>
<CssBaseline />
<NavigationBar />
<SideBar />
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
<Toolbar />
{children}
</Box>
</Box>
</AppRouterCacheProvider>
</body>
</html>
);
}
いかがでしょうか。layout.tsxがだいぶスッキリしました。zIndexの指定はnavigationbar.tsxの中で行われているので、navigationbar.tsxだけ"use client";
を指定しています。
これで見た目は管理画面っぽいものができました。ただしこのままではサイドバーのメニューをクリックしても何も起きません。メニューをクリックしたらページ遷移をするようにしたいです。
長くなったので今回はここまで。次回はページ遷移を実装していきましょう。
サンプルコード
今回紹介したコードは以下で公開しています。
Discussion