はじめに
以下の記事では、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>
=============== 以下略 ===============
サイドメニューは一度仕様が決まってしまえば変更が生じることはあまりないと思います。
なのでこのままでも良いような気もしますが、こういうのはコンポーネント化しておくに越したことはありません。
もし変更があった場合は同じ修正を何箇所にも適用することになりますし、変更し忘れも普通に起こります。
バグの温床となるので、私はやっぱりコンポーネント化する派です。
というわけで、今回は冗長な処理をコンポーネント化してみました。
実物はこちら
リポジトリはこちら
概要
冗長なコードをまとめただけなので、画面の見た目は特に変わりません。
以下のケースを想定して作成しています。
- ロゴと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を作ってみてほしい」「こんな場合はどうすれば?」といったアイディアを募集中です!
コメントお待ちしております。
コメント