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

目次

はじめに

以下の記事では、4つの項目を持つスタティックなサイドメニューを実装しました。

こちらのコードを見ると一目瞭然なのですが、4つの項目を出力するコードは同じ処理をコピペしており冗長になっています。

                <ul>
                    <Box
                        component="li"
                        pl={5}
                        py={2}
                        sx={{
                            borderBottom: '1px solid white',
                            '&:hover': {
                                opacity: 0.7
                            },
                            backgroundColor: pathname === '/product' ? '#afaffa' : 'transparent'
                        }}
                        onClick={() => setPathname('/product')}
                    >
                        <HomeRepairServiceIcon
                            fontSize="large"
                            sx={{
                                color: "white",
                                marginRight: "0.5rem",
                                position: 'relative',
                                top: -3
                            }}
                        />
                        <Typography
                            variant="subtitle1"
                            component="span"
                            sx={{
                                color: "white"
                            }}
                        >
                            Product
                        </Typography>
                    </Box>
                    <Box
                        component="li"
                        pl={5}
                        py={2}
                        sx={{
                            borderBottom: '1px solid white',
                            '&:hover': {
                                opacity: 0.7
                            },
                            backgroundColor: pathname === '/work-schedule' ? '#afaffa' : 'transparent'
                        }}
                        onClick={() => setPathname('/work-schedule')}
                    >
                        <CalendarMonthIcon
                            fontSize="large"
                            sx={{
                                color: "white",
                                marginRight: "0.5rem",
                                position: 'relative',
                                top: -3
                            }}
                        />
                        <Typography
                            variant="subtitle1"
                            component="span"
                            sx={{
                                color: "white"
                            }}
                        >
                            Work Schedule
                        </Typography>
                    </Box>
=============== 以下略 ===============

サイドメニューは一度仕様が決まってしまえば変更が生じることはあまりないと思います。
なのでこのままでも良いような気もしますが、こういうのはコンポーネント化しておくに越したことはありません。

もし変更があった場合は同じ修正を何箇所にも適用することになりますし、変更し忘れも普通に起こります。
バグの温床となるので、私はやっぱりコンポーネント化する派です。

というわけで、今回は冗長な処理をコンポーネント化してみました。

実物はこちら

あわせて読みたい
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, Typography} from "@mui/material";
import LocalConvenienceStoreIcon from '@mui/icons-material/LocalConvenienceStore';
import HomeRepairServiceIcon from '@mui/icons-material/HomeRepairService';
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
import PeopleIcon from '@mui/icons-material/People';
import BuildIcon from '@mui/icons-material/Build';
//import {usePathname} from "next/navigation";
import React, {MouseEventHandler, ReactElement, useState} from "react";
import {SvgIconComponent} from "@mui/icons-material";

const Logo = () => (
    <Box
        p={2}
        sx={{
            backgroundColor: "#6573c3",
            textWrap: "nowrap"
        }}
    >
        <LocalConvenienceStoreIcon
            fontSize="large"
            sx={{
                color: "white",
                marginRight: "0.5rem",
                position: 'relative',
                top: -5
            }}
        />
        <Typography
            variant="h5"
            component="span"
            sx={{
                color: "white"
            }}
        >
            Shop System
        </Typography>
    </Box>
)

const MenuIcon = ({MuiIcon}: {MuiIcon: SvgIconComponent}) => {
    const iconStyle = {
        color: "white",
        marginRight: "0.5rem",
        position: 'relative',
        top: -3
    }
    return (
        <MuiIcon fontSize="large" sx={{...iconStyle}}/>
    )
}

const MenuItem = ({itemName, onClick, Icon, isHighlight}: {itemName: string, onClick: MouseEventHandler<HTMLLIElement>, Icon: ReactElement, isHighlight: boolean}) => {
    return (
        <Box
            component="li"
            pl={5}
            py={2}
            sx={{
                borderBottom: '1px solid white',
                '&:hover': {
                    opacity: 0.7
                },
                backgroundColor: isHighlight ? '#afaffa' : 'transparent'
            }}
            onClick={onClick}
        >
            {Icon}
            <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>
                    <MenuItem
                        itemName="Product"
                        onClick={() => setPathname('/product')}
                        Icon={<MenuIcon MuiIcon={HomeRepairServiceIcon}/>}
                        isHighlight={pathname === '/product'}
                    />
                    <MenuItem
                        itemName="Work Schedule"
                        onClick={() => setPathname('/work-schedule')}
                        Icon={<MenuIcon MuiIcon={CalendarMonthIcon}/>}
                        isHighlight={pathname === '/work-schedule'}
                    />
                    <MenuItem
                        itemName="Staff"
                        onClick={() => setPathname('/staff')}
                        Icon={<MenuIcon MuiIcon={PeopleIcon}/>}
                        isHighlight={pathname === '/staff'}
                    />
                    <MenuItem
                        itemName="System Setting"
                        onClick={() => setPathname('/system-setting')}
                        Icon={<MenuIcon MuiIcon={BuildIcon}/>}
                        isHighlight={pathname === '/system-setting'}
                    />
                </ul>
            </Box>
            <Box
                component="main"
                sx={{
                    backgroundColor: "#fffafa"
                }}
                width="100%"
                m={2}
            >
                <Typography variant="h3" sx={{ color: "#6573c3"}}>
                    {
                        getPageName()
                    }
                </Typography>
            </Box>
        </Box>

    )
}

各部分の解説

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

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

const MenuIcon = ({MuiIcon}: {MuiIcon: SvgIconComponent}) => {
    const iconStyle = {
        color: "white",
        marginRight: "0.5rem",
        position: 'relative',
        top: -3
    }
    return (
        <MuiIcon fontSize="large" sx={{...iconStyle}}/>
    )
}

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

コンポーネント化するときは、同じ処理の中でも違う部分はどこだろう?と考えるとよいと思います。

コンポーネント化する前のコードを再掲しますが、同じコードのうち以下の部分が異なっていることを確認してください。

  • 項目名(Product、Staff、Work Schedule、System Setting)
  • 項目名をクリックしたときの処理
  • アイコン
  • ハイライトする条件
                <ul>
                    <Box
                        component="li"
                        pl={5}
                        py={2}
                        sx={{
                            borderBottom: '1px solid white',
                            '&:hover': {
                                opacity: 0.7
                            },
                            backgroundColor: pathname === '/product' ? '#afaffa' : 'transparent'
                        }}
                        onClick={() => setPathname('/product')}
                    >
                        <HomeRepairServiceIcon
                            fontSize="large"
                            sx={{
                                color: "white",
                                marginRight: "0.5rem",
                                position: 'relative',
                                top: -3
                            }}
                        />
                        <Typography
                            variant="subtitle1"
                            component="span"
                            sx={{
                                color: "white"
                            }}
                        >
                            Product
                        </Typography>
                    </Box>
                    <Box
                        component="li"
                        pl={5}
                        py={2}
                        sx={{
                            borderBottom: '1px solid white',
                            '&:hover': {
                                opacity: 0.7
                            },
                            backgroundColor: pathname === '/work-schedule' ? '#afaffa' : 'transparent'
                        }}
                        onClick={() => setPathname('/work-schedule')}
                    >
                        <CalendarMonthIcon
                            fontSize="large"
                            sx={{
                                color: "white",
                                marginRight: "0.5rem",
                                position: 'relative',
                                top: -3
                            }}
                        />
                        <Typography
                            variant="subtitle1"
                            component="span"
                            sx={{
                                color: "white"
                            }}
                        >
                            Work Schedule
                        </Typography>
                    </Box>
=============== 以下略 ===============

これらの相違点を props で受け取るようにすることでコンポーネント化しました。

コンポーネントの呼び出し側

作成したコンポーネントは以下のように呼び出すことができます。

                <ul>
                    <MenuItem
                        itemName="Product"
                        onClick={() => setPathname('/product')}
                        Icon={<MenuIcon MuiIcon={HomeRepairServiceIcon}/>}
                        isHighlight={pathname === '/product'}
                    />
                    <MenuItem
                        itemName="Work Schedule"
                        onClick={() => setPathname('/work-schedule')}
                        Icon={<MenuIcon MuiIcon={CalendarMonthIcon}/>}
                        isHighlight={pathname === '/work-schedule'}
                    />
                    <MenuItem
                        itemName="Staff"
                        onClick={() => setPathname('/staff')}
                        Icon={<MenuIcon MuiIcon={PeopleIcon}/>}
                        isHighlight={pathname === '/staff'}
                    />
                    <MenuItem
                        itemName="System Setting"
                        onClick={() => setPathname('/system-setting')}
                        Icon={<MenuIcon MuiIcon={BuildIcon}/>}
                        isHighlight={pathname === '/system-setting'}
                    />
                </ul>

page.tsx の SideMenuコンポーネントが MenuItemコンポーネントと MenuIconコンポーネントの二つに依存してしまっているので、できれば MenuItemコンポーネントへの依存に抑えたいところですね。
これに関しては型定義がうまくいかず、いまの私にはできませんでした・・・。
詳しい方はご助言いただけると嬉しいです。

おわりに

いかがだったでしょうか?
今回はサイドメニューの冗長な処理をコンポーネント化してみました。

app/side-menu/page.tsx のコード行数が 218行だったのに対し、app/side-menu-component/page.tsx は 156行でした。
およそ20%ちょっとの削減に成功しましたね。

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

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

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

この記事を書いた人

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

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

コメント

コメントする

目次