6
9

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-11-01

完成品

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

Drawerの「データ追加」をクリックすると以下の画面が表示されます。
image.png

Drawerの「データ一覧」をクリックすると以下の画面が表示されます。
image.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のサンプルコードを利用しています。
注意すべきは以下のように、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が重複するためワーニングが出ます。
また、型定義が不十分な箇所があります。
今後、ゆっくりではありますが修正予定ですのでご容赦ください。
また、お気づきの点はコメントで指摘いただければと存じます。

6
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
9