【React】Material-UIのDrawerを使ったページ遷移

## 完成品

以下のような画面を作成します。
左側にdrawerを表示し、「データ追加」、「データ一覧」をクリックするとそれぞれの画面へ遷移する、よくあるメニュー画面です。

Drawerの「データ追加」をクリックすると以下の画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517043/bb433ab9-e90f-3d14-85ea-65fc07151c4d.png)

Drawerの「データ一覧」をクリックすると以下の画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517043/28510353-b7b4-de26-e3cd-4d4fe8c12a74.png)

意外と苦労してしまったので、記録しておきます。
ベストプラクティスと異なれば、ぜひご教授下さい。



## 前提
以下のバージョンを利用しています。

- @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
以下のようにルーティングを記述しています。
※以下コードは一部抜粋です

```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](https://material-ui.com/components/drawers/#mini-variant-drawer)のサンプルコードを利用しています。
注意すべきは以下のように、`React.ReactNode`型の変数を利用しているところです。
※以下コードは一部抜粋です

```Menu.tsx
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が適用されたコンポーネントとなります。

```ListData.tsx
const ListData: React.FC<Props> = () => {
    return (
        <Menu>
            <h1>list</h1>
        </Menu>
    );
};
```

```RegisterData.tsx
const RegisterData: React.FC = () => {
    return (
        <Menu>
            <h1>add</h1>
        </Menu>
    );
};
```


##  参考: コード全貌

```App.tsx
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;
```

```Menu.tsx
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;
```

```ListData.tsx
import React from 'react';
import Menu from './Menu';


type Props = {};

const ListData: React.FC<Props> = () => {
    return (
        <Menu>
            <h1>list</h1>
        </Menu>
    );
};

export default ListData;
```

```RegisterData.tsx
import React from 'react';
import Menu from './Menu';


const RegisterData: React.FC = () => {
    return (
        <Menu>
            <h1>add</h1>
        </Menu>
    );
};

export default RegisterData;
```

## お詫び

上記コードは、コンポーネント名`Menu`とinterface名`Menu`が重複するためワーニングが出ます。
また、型定義が不十分な箇所があります。
今後、ゆっくりではありますが修正予定ですのでご容赦ください。
また、お気づきの点はコメントで指摘いただければと存じます。