Reactで作ったサイドメニューの冗長な箇所をコンポーネント化したので動的出力する

目次

はじめに

以下の記事では、サイドメニューの4つの項目を出力する処理が冗長だったのでコンポーネント化しました。

コンポーネント化したことで身動きがとりやすくなったので、もう1段階レベルアップさせたいと思います。

今回は、サイドメニューの項目を動的に出力するようにしてみました。

実物はこちら

あわせて読みたい
React Recipe Reactアプリのレシピ集

リポジトリはこちら

GitHub
GitHub - hakoratory/react-recipe Contribute to hakoratory/react-recipe development by creating an account on GitHub.

概要

動的出力するにあたりアイコンの指定の仕方を変更したので、見た目が若干異なっています。

以下のケースを想定して作成しています。

  • ロゴと4つの項目があり、今後増えたり減ったりする可能性がある
  • 項目を選択すると、対応する画面が右エリアに表示される

実装

先にコード全容です。

'use client'
import {Box, Icon, Typography} from "@mui/material";
//import {usePathname} from "next/navigation";
import React, {MouseEventHandler, useState} from "react";

const Logo = () => (
    <Box
        p={2}
        sx={{
            backgroundColor: "#6573c3",
            textWrap: 'nowrap',
            display: 'flex',
            alignItems: 'center',
        }}
    >
        <Icon
            sx={{
                fontSize: '2rem',
                color: 'white',
                marginRight: "0.5rem",
            }}
        >
            local_convenience_store
        </Icon>
        <Typography
            variant="h5"
            component="span"
            sx={{
                color: "white"
            }}
        >
            Shop System
        </Typography>
    </Box>
)

const menuItems = [
    {
        url: '/product',
        itemName: 'Product',
        icon: 'home_repair_service'
    },
    {
        url: '/work-schedule',
        itemName: 'Work Schedule',
        icon: 'calendar_month'
    },
    {
        url: '/staff',
        itemName: 'Staff',
        icon: 'people'
    },
    {
        url: '/system-setting',
        itemName: 'System Setting',
        icon: 'build'
    },
]

const MenuIcon = ({iconName}: { iconName: string }) => {
    const iconStyle = {
        fontSize: '1.8rem',
        color: "white",
        marginRight: "0.5rem",
    }
    return (
        <Icon sx={{...iconStyle}}>{iconName}</Icon>
    )
}

const MenuItem = ({itemName, onClick, iconName, isHighlight}: {
    itemName: string,
    onClick: MouseEventHandler<HTMLLIElement>,
    iconName: string,
    isHighlight: boolean
}) => {
    return (
        <Box
            component="li"
            pl={5}
            py={2}
            sx={{
                borderBottom: '1px solid white',
                '&:hover': {
                    opacity: 0.7
                },
                backgroundColor: isHighlight ? '#afaffa' : 'transparent',
                display: 'flex',
                alignItems: 'center',
            }}
            onClick={onClick}
        >
            <MenuIcon iconName={iconName}/>
            <Typography
                variant="subtitle1"
                component="span"
                sx={{
                    color: "white",
                }}
            >
                {itemName}
            </Typography>
        </Box>
    )
}

export default function SideMenu() {
    //const pathname = usePathname()
    const [pathname, setPathname] = useState<string>('/');

    function getPageName() {
        switch (pathname) {
            case '/product':
                return 'Product'
            case '/work-schedule':
                return 'Work Schedule'
            case '/staff':
                return 'Staff'
            case '/system-setting':
                return 'System Setting'
            default:
                return ''
        }
    }

    return (
        <Box display="flex">
            <Box
                component="nav"
                height="100vh"
                bgcolor="primary"
                style={{
                    backgroundColor: "#7c88cc"
                }}
            >
                <Logo/>
                <ul>
                    {
                        menuItems.map((menuItem, index) => (
                            <MenuItem
                                key={index}
                                itemName={menuItem.itemName}
                                onClick={() => setPathname(menuItem.url)}
                                iconName={menuItem.icon}
                                isHighlight={pathname === menuItem.url}
                            />
                        ))
                    }
                </ul>
            </Box>
            <Box
                component="main"
                sx={{
                    backgroundColor: "#fffafa"
                }}
                width="100%"
                m={2}
            >
                <Typography variant="h3" sx={{color: "#6573c3"}}>
                    {
                        getPageName()
                    }
                </Typography>
            </Box>
        </Box>

    )
}

各部分の解説

動的出力のため、サイドメニューの項目をオブジェクトで定義する

サイドメニューの項目を定義するオブジェクトを作り、これを使って動的に出力します。

const menuItems = [
    {
        url: '/product',
        itemName: 'Product',
        icon: 'home_repair_service'
    },
    {
        url: '/work-schedule',
        itemName: 'Work Schedule',
        icon: 'calendar_month'
    },
    {
        url: '/staff',
        itemName: 'Staff',
        icon: 'people'
    },
    {
        url: '/system-setting',
        itemName: 'System Setting',
        icon: 'build'
    },
]

url itemName icon の3つのプロパティがあることを確認してください。

icon は名前のとおり、どのアイコンを表示するかを定義しています。
前回は、コンポーネントを props で渡す形をとっていましたが、文字列で指定できるように変更しています(後述)。

MenuItemコンポーネントとMenuIconコンポーネント

サイドメニューの各項目は MenuItemコンポーネントとして抽出しています。
さらにアイコン部分はスタイル指定を共通化したかったので MenuIconコンポーネントとして抽出しています。

作りは前回と大きく変わりませんが、MenuIconコンポーネントの引数が iconName という文字列を受け取る形になっていることを確認してください。

const MenuIcon = ({iconName}: { iconName: string }) => {
    const iconStyle = {
        fontSize: '1.8rem',
        color: "white",
        marginRight: "0.5rem",
    }
    return (
        <Icon sx={{...iconStyle}}>{iconName}</Icon>
    )
}

const MenuItem = ({itemName, onClick, iconName, isHighlight}: {
    itemName: string,
    onClick: MouseEventHandler<HTMLLIElement>,
    iconName: string,
    isHighlight: boolean
}) => {
    return (
        <Box
            component="li"
            pl={5}
            py={2}
            sx={{
                borderBottom: '1px solid white',
                '&:hover': {
                    opacity: 0.7
                },
                backgroundColor: isHighlight ? '#afaffa' : 'transparent',
                display: 'flex',
                alignItems: 'center',
            }}
            onClick={onClick}
        >
            <MenuIcon iconName={iconName}/>
            <Typography
                variant="subtitle1"
                component="span"
                sx={{
                    color: "white",
                }}
            >
                {itemName}
            </Typography>
        </Box>
    )
}

アイコンを名前で指定する

これまでは、使いたいアイコンのコンポーネントをインポートして使っていました。
以下のように名前でアイコンを指定する方法もあります。

あわせて読みたい
React Icon Component - Material UI Guidance and suggestions for using icons with Material UI.

マニュアルに記載されているように、Google Web Font を読み込んでおく必要があります。

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
    <head>
      <link
          rel="stylesheet"
          href="https://fonts.googleapis.com/icon?family=Material+Icons"
      />
    </head>

少し横道にそれますが、Next.js の v13 以降では、head タグへの出力は Metadata API で行うようになりました。

export const metadata: Metadata = {
  title: "React Recipe",
  description: "Reactアプリのレシピ集"
};

export default function RootLayout({

しかし、rel="stylesheet" は Unsupported とのことなのでこのように直接記載しています。

あわせて読みたい
Functions: generateMetadata | Next.js Learn how to add Metadata to your Next.js application for improved search engine optimization (SEO) and web shareability.

サイドメニューの項目を動的に出力する

以上でサイドメニューの項目を動的に出力する準備が整いました。
あとは map で出力すれば完成です。

                <ul>
                    {
                        menuItems.map((menuItem, index) => (
                            <MenuItem
                                key={index}
                                itemName={menuItem.itemName}
                                onClick={() => setPathname(menuItem.url)}
                                iconName={menuItem.icon}
                                isHighlight={pathname === menuItem.url}
                            />
                        ))
                    }
                </ul>

おわりに

いかがだったでしょうか?
今回はサイドメニューの項目を動的に出力してみました。

サイドメニューの項目をオブジェクトで定義することで、柔軟に設定ができるようになります。
以下は、ログインユーザーの権限によって表示を変える設定を追加した例です。

const menuItems = [
    {
        url: '/product',
        itemName: 'Product',
        icon: 'home_repair_service',
        isShowAdmin: true, // Admin のときのみ表示する
        isDisabledUser: true, // User のときは非活性
    },

「こんなこともできるかも!」と閃いたそこのあなた。
忘れないうちにぜひ形にしてみてくださいね。

ハコラトリは、Reactを習得したい新米エンジニアを応援しています。

Reactレシピでは、これからもReactで色々なUIを作って紹介していきますので、「こんなUIを作ってみてほしい」「こんな場合はどうすれば?」といったアイディアを募集中です!
コメントお待ちしております。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

28歳のときに音楽家からエンジニアに転向。
WEB系のシステム開発(Java, C#, Vue, React など)に携わっています。

自分らしく力を発揮できて、自信を持って生きられる人を増やすための活動をしています。

コメント

コメントする

目次