😺

デザインコンポーネントの MUI を React に適応してみた

2023/04/16に公開

はじめに

今回は React コンポーネントライブラリ MUI(旧 Material UI)を vite で作った React プロジェクトに適応して簡単なダッシュボード風管理画面を作っていきます。

コンポーネントライブラリについて書いた方がいいかなと思ったので次回はコンポーネントライブラリについて書きますが、今回はその実装についてです。

下準備

ディレクトリ構成

コンポーネントの分け方は Atomic Design

src/
 ├ components/
 │ └ atoms/        # 原子(個々のパーツ)
 │ └ molecules/    # 分子(原子の集合体)
 │ └ organisms/    # 生体(分子の集合体)
 │ └ templates/    # テンプレート(ページの雛形)
 │ └ pages/        # ページ
 ├ App.tsx
...

ページを作成

サンプルページとして src/components/pages/に HomePage.tsx と ProductPage.tsx を作成します。

HomePage.tsx
import React from "react";

const HomePage: React.VFC = () => {
  return <>トップページ</>;
};

export default HomePage;
ProductPage.tsx
import React from "react";

const ProductPage: React.VFC = () => {
  return <>商品ページ</>;
};

export default ProductPage;

ルーティング

指定のパスにアクセスした際、先ほど作成した 2 つのページが描写されるようにルーティングを実装してみます。

まずはルーティングを実装するためのライブラリをインストールします。

npm i react-router-dom
npm i -D @types/react-router-dom

App.tsx を編集してルーティングを実装します。

App.tsx
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";

import ProductPage from "./components/pages/ProductPage";
import HomePage from "./components/pages/HomePage";

const App: React.VFC = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/products" element={ProductPage} />
        <Route path="/" element={HomePage} />
      </Routes>
    </BrowserRouter>
  );
};

export default App;

npm run devで起動して「トップページ」と表示されれば成功です。

npm run dev

http://localhost:3000/

http://localhost:3000/products

MUI を導入

下準備がおわったのでメインの MUI 導入を行っていきます。

MUI をインストール

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

テンプレートを作成

src/components/templates/にページのテンプレートを作成します。MUI が公開しているテンプレートを参考に実装してみます。

import * as React from "react";
import { styled, createTheme, ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import MuiDrawer from "@mui/material/Drawer";
import Box from "@mui/material/Box";
import MuiAppBar, { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import List from "@mui/material/List";
import Typography from "@mui/material/Typography";
import Divider from "@mui/material/Divider";
import Container from "@mui/material/Container";
import { Link } from "react-router-dom";
import MenuIcon from "@mui/icons-material/Menu";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import IconButton from "@mui/material/IconButton";
import HomeIcon from "@mui/icons-material/Home";
import ShoppingCartIcon from "@mui/icons-material/ShoppingCart";
import ListItem from "@mui/material/ListItem";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";

const drawerWidth = 240;

interface AppBarProps extends MuiAppBarProps {
  open?: boolean;
}

const AppBar = styled(MuiAppBar, {
  shouldForwardProp: (prop) => prop !== "open",
})<AppBarProps>(({ theme, open }) => ({
  zIndex: theme.zIndex.drawer + 1,
  transition: theme.transitions.create(["width", "margin"], {
    easing: theme.transitions.easing.sharp,
    duration: theme.transitions.duration.leavingScreen,
  }),
  ...(open && {
    marginLeft: drawerWidth,
    width: `calc(100% - ${drawerWidth}px)`,
    transition: theme.transitions.create(["width", "margin"], {
      easing: theme.transitions.easing.sharp,
      duration: theme.transitions.duration.enteringScreen,
    }),
  }),
}));
const Copyright = () => {
  return (
    <Typography variant="body2" color="textSecondary" align="center">
      {"Copyright © "}
      <Link color="inherit" to="/">
        管理画面
      </Link> {new Date().getFullYear()}
      {"."}
    </Typography>
  );
};

const Drawer = styled(MuiDrawer, {
  shouldForwardProp: (prop) => prop !== "open",
})(({ theme, open }) => ({
  "& .MuiDrawer-paper": {
    position: "relative",
    whiteSpace: "nowrap",
    width: drawerWidth,
    transition: theme.transitions.create("width", {
      easing: theme.transitions.easing.sharp,
      duration: theme.transitions.duration.enteringScreen,
    }),
    boxSizing: "border-box",
    ...(!open && {
      overflowX: "hidden",
      transition: theme.transitions.create("width", {
        easing: theme.transitions.easing.sharp,
        duration: theme.transitions.duration.leavingScreen,
      }),
      width: theme.spacing(7),
      [theme.breakpoints.up("sm")]: {
        width: theme.spacing(9),
      },
    }),
  },
}));

const mdTheme = createTheme();

export interface GenericTemplateProps {
  children: React.ReactNode;
  title: string;
}

const GenericTemplate: React.FC<GenericTemplateProps> = ({
  children,
  title,
}) => {
  const [open, setOpen] = React.useState(true);
  const toggleDrawer = () => {
    setOpen(!open);
  };

  return (
    <ThemeProvider theme={mdTheme}>
      <Box sx={{ display: "flex" }}>
        <CssBaseline />
        <AppBar position="absolute" open={open}>
          <Toolbar
            sx={{
              pr: "24px", // keep right padding when drawer closed
            }}
          >
            <IconButton
              edge="start"
              color="inherit"
              aria-label="open drawer"
              onClick={toggleDrawer}
              sx={{
                marginRight: "36px",
                ...(open && { display: "none" }),
              }}
            >
              <MenuIcon />
            </IconButton>
            <Typography
              component="h1"
              variant="h6"
              color="inherit"
              noWrap
              sx={{ flexGrow: 1 }}
            >
              管理画面
            </Typography>
          </Toolbar>
        </AppBar>
        <Drawer variant="permanent" open={open}>
          <Toolbar
            sx={{
              display: "flex",
              alignItems: "center",
              justifyContent: "flex-end",
              px: [1],
            }}
          >
            <IconButton onClick={toggleDrawer}>
              <ChevronLeftIcon />
            </IconButton>
          </Toolbar>
          <Divider />
          <List component="nav">
            <Link to="/">
              <ListItem button>
                <ListItemIcon>
                  <HomeIcon />
                </ListItemIcon>
                <ListItemText primary="トップページ" />
              </ListItem>
            </Link>
            <Link to="/products">
              <ListItem button>
                <ListItemIcon>
                  <ShoppingCartIcon />
                </ListItemIcon>
                <ListItemText primary="商品ページ" />
              </ListItem>
            </Link>
          </List>
        </Drawer>
        <Box
          component="main"
          sx={{
            backgroundColor: (theme) =>
              theme.palette.mode === "light"
                ? theme.palette.grey[100]
                : theme.palette.grey[900],
            flexGrow: 1,
            height: "100vh",
            overflow: "auto",
          }}
        >
          <Toolbar />
          <Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
            <Typography component="h2" variant="h5" color="inherit" noWrap>
              {title}
            </Typography>
            {children}
            <Box pt={4}>
              <Copyright />
            </Box>
          </Container>
        </Box>
      </Box>
    </ThemeProvider>
  );
};

export default GenericTemplate;

テンプレートを使う

トップページと商品ページにテンプレートを適応します。

HomePage.tsx
import React from "react";
import GenericTemplate from "../templates/GenericTemplate";

const HomePage: React.VFC = () => {
  return (
    <GenericTemplate title="トップページ">
      <>トップページ内容</>
    </GenericTemplate>
  );
};

export default HomePage;
ProductPage.tsx
import React from "react";
import GenericTemplate from "../templates/GenericTemplate";

const ProductPage: React.VFC = () => {
  return (
    <GenericTemplate title="商品ページ">
      <>商品ページ内容</>
    </GenericTemplate>
  );
};

export default ProductPage;

ブラウザでテンプレートが反映されていることを確認します。

コンポーネントを使う

MUI には様々なコンポーネントが用意されていて、コンポーネントを利用することで効率よく開発することが出来ます。よく使いそうなのテーブルを実装してみます。

import React from "react";
import GenericTemplate from "../templates/GenericTemplate";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";

const createData = (
  name: string,
  category: string,
  weight: number,
  price: number
) => {
  return { name, category, weight, price };
};

const rows = [
  createData("チョコレート", "お菓子", 100, 120),
  createData("ケーキ", "お菓子", 400, 480),
  createData("りんご", "フルーツ", 500, 360),
  createData("バナナ", "フルーツ", 200, 300),
  createData("みかん", "フルーツ", 250, 180),
];

const ProductPage: React.FC = () => {
  return (
    <GenericTemplate title="商品ページ">
      <TableContainer component={Paper}>
        <Table>
          <TableHead>
            <TableRow>
              <TableCell>商品名</TableCell>
              <TableCell align="right">カテゴリー</TableCell>
              <TableCell align="right">重量(g)</TableCell>
              <TableCell align="right">価格(円)</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {rows.map((row) => (
              <TableRow key={row.name}>
                <TableCell component="th" scope="row">
                  {row.name}
                </TableCell>
                <TableCell align="right">{row.category}</TableCell>
                <TableCell align="right">{row.weight}</TableCell>
                <TableCell align="right">{row.price}</TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </TableContainer>
    </GenericTemplate>
  );
};

export default ProductPage;

参考

Discussion