完成品
以下のような画面を作成します。
左側にdrawerを表示し、「データ追加」、「データ一覧」をクリックするとそれぞれの画面へ遷移する、よくあるメニュー画面です。
Drawerの「データ追加」をクリックすると以下の画面が表示されます。
Drawerの「データ一覧」をクリックすると以下の画面が表示されます。
意外と苦労してしまったので、記録しておきます。
ベストプラクティスと異なれば、ぜひご教授下さい。
前提
以下のバージョンを利用しています。
- @material-ui/core": 4.11.0
- @material-ui/icons": 4.9.1
- "react": 17.0.1
- "react-dom": 17.0.1
- "react-router-dom": 5.2.0
掲載漏れあればコメントください。
コンポーネント
以下のコンポーネントが登場します。
- App.tsx
主にルーティングを定義しています - ./components/Menu.tsx
メニューバー・Drawerを形成しています - ./components/ListData.tsx
ページに表示される内容その1です - ./components/RegisterData.tsx
ページに表示される内容その2です
App.tsx
以下のようにルーティングを記述しています。
※以下コードは一部抜粋です
function App() {
return (
<div className="App">
<Router>
<Switch>
<Route path="/" component={ListData} exact />
<Route path="/list" component={ListData} exact />
<Route path="/add" component={RegisterData} exact />
</Switch>
</Router>
</div>
);
}
Menu.tsx
基本的にはMaterial-UIのMini variant drawerのサンプルコードを利用しています。
注意すべきは以下のように、React.ReactNode
型の変数を利用しているところです。
※以下コードは一部抜粋です
export interface Menu {
children: React.ReactNode;
}
const Menu: React.FC<Menu> = ({ children }) => {
略
return (
<Drawer
variant="permanent"
className={clsx(classes.drawer, {
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
}),
}}
>
<div className={classes.toolbar}>
<IconButton onClick={handleDrawerClose}>
{theme.direction === 'rtl' ? <ChevronRightIcon /> : <ChevronLeftIcon />}
</IconButton>
</div>
<Divider />
<List>
<Link to="/add" className={classes.link}>
<ListItem button>
<ListItemIcon>
<AddIcon />
</ListItemIcon>
<ListItemText primary="データ追加" />
</ListItem>
</Link>
<Link to="/list" className={classes.link}>
<ListItem button>
<ListItemIcon>
<ListIcon />
</ListItemIcon>
<ListItemText primary="データ一覧" />
</ListItem>
</Link>
</List>
<Divider />
</Drawer>
<main className={classes.content}>
<div className={classes.toolbar} />
{children}
</main>
);
ListData.tsx、RegisterData.tsx
それぞれ以下のように、内容を<Menu>
の内部に記述することで、メニューバー・Drawerが適用されたコンポーネントとなります。
const ListData: React.FC<Props> = () => {
return (
<Menu>
<h1>list</h1>
</Menu>
);
};
const RegisterData: React.FC = () => {
return (
<Menu>
<h1>add</h1>
</Menu>
);
};
参考: コード全貌
import React from 'react';
import RegisterData from './components/RegisterData';
import ListData from './components/ListData';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import './App.css';
function App() {
return (
<div className="App">
<Router>
<Switch>
<Route path="/" component={ListData} exact />
<Route path="/list" component={ListData} exact />
<Route path="/add" component={RegisterData} exact />
</Switch>
</Router>
</div>
);
}
export default App;
import React from 'react';
import clsx from 'clsx';
import { createStyles, makeStyles, useTheme, Theme } from '@material-ui/core/styles';
import Drawer from '@material-ui/core/Drawer';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import List from '@material-ui/core/List';
import CssBaseline from '@material-ui/core/CssBaseline';
import Typography from '@material-ui/core/Typography';
import Divider from '@material-ui/core/Divider';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft';
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import ListIcon from '@material-ui/icons/List';
import AddIcon from '@material-ui/icons/Add';
import { Link } from "react-router-dom";
export interface Menu {
children: React.ReactNode;
}
const drawerWidth = 240;
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
display: 'flex',
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
},
appBarShift: {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
menuButton: {
marginRight: 36,
},
hide: {
display: 'none',
},
drawer: {
width: drawerWidth,
flexShrink: 0,
whiteSpace: 'nowrap',
},
drawerOpen: {
width: drawerWidth,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
drawerClose: {
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: 'hidden',
width: theme.spacing(7) + 1,
[theme.breakpoints.up('sm')]: {
width: theme.spacing(9) + 1,
},
},
toolbar: {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar,
},
content: {
flexGrow: 1,
padding: theme.spacing(3),
},
link: {
textDecoration: "none",
color: theme.palette.text.secondary,
},
}),
);
const Menu: React.FC<Menu> = ({ children }) => {
const classes = useStyles();
const theme = useTheme();
const [open, setOpen] = React.useState(false);
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
return (
<div className={classes.root}>
<CssBaseline />
<AppBar
position="fixed"
className={clsx(classes.appBar, {
[classes.appBarShift]: open,
})}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
edge="start"
className={clsx(classes.menuButton, {
[classes.hide]: open,
})}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap>
My App
</Typography>
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
className={clsx(classes.drawer, {
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
}),
}}
>
<div className={classes.toolbar}>
<IconButton onClick={handleDrawerClose}>
{theme.direction === 'rtl' ? <ChevronRightIcon /> : <ChevronLeftIcon />}
</IconButton>
</div>
<Divider />
<List>
<Link to="/add" className={classes.link}>
<ListItem button>
<ListItemIcon>
<AddIcon />
</ListItemIcon>
<ListItemText primary="データ追加" />
</ListItem>
</Link>
<Link to="/list" className={classes.link}>
<ListItem button>
<ListItemIcon>
<ListIcon />
</ListItemIcon>
<ListItemText primary="データ一覧" />
</ListItem>
</Link>
</List>
<Divider />
</Drawer>
<main className={classes.content}>
<div className={classes.toolbar} />
{children}
</main>
</div>
);
}
export default Menu;
import React from 'react';
import Menu from './Menu';
type Props = {};
const ListData: React.FC<Props> = () => {
return (
<Menu>
<h1>list</h1>
</Menu>
);
};
export default ListData;
import React from 'react';
import Menu from './Menu';
const RegisterData: React.FC = () => {
return (
<Menu>
<h1>add</h1>
</Menu>
);
};
export default RegisterData;
お詫び
上記コードは、コンポーネント名Menu
とinterface名Menu
が重複するためワーニングが出ます。
また、型定義が不十分な箇所があります。
今後、ゆっくりではありますが修正予定ですのでご容赦ください。
また、お気づきの点はコメントで指摘いただければと存じます。