【備忘録】日本一わかりやすいReact-Redux講座 実践編 #10 「Drawerメニューを作ろう」

## はじめに
### 概要
この記事は、トラハック氏([@torahack_](https://twitter.com/torahack_))が運営するYoutubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『[日本一わかりやすいReact-Redux講座 実践編](https://www.youtube.com/watch?v=8fNgXnVTULU)』の学習備忘録です。

前回の講座で、ヘッダーのビューを作りました

今回の講座では、Material-UIの``<Drawer>``コンポーネントを用いて、ヘッダーメニューの動的な挙動を実装します。

※ 前回記事: [【備忘録】日本一わかりやすいReact-Redux講座 実践編 #9 「ネイティブアプリ風ヘッダーを作ろう」](https://qiita.com/ddpmntcpbr/items/cefee2b9fd0237a24b0f)


### 動画URL
[Drawerメニューを作ろう【日本一わかりやすいReact-Redux講座 実践編#10】](https://www.youtube.com/watch?v=bw_oIwcIGA0&t=1s)

### 要点
- Material-UIの``<Drawer>``コンポーネントを用いて、ネイティブアプリ風のDrawer Headerを作成できる。
- Drawerメニューの実装には、開閉の状態を決定する state と、それを制御する関数を定義して``<Drawer>``コンポーネントに渡す。

### 完成系イメージ

**``http://localhost:3000``**

![10-2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/eae91a80-52da-15ad-d647-3fb6292fcc62.gif)

ヘッダーのメニューアイコンをクリックすることで、Drawer Menuを表示させられます。

キーワード欄はダミーで、今はただのテキストフィールドです。最終的にはここで商品情報の検索を行えるようにします。

メニューアイテムの「商品情報」「注文履歴」「プロフィール」をクリックすることで、対応するページへ遷移します。今は「注文履歴」「プロフィール」についてはテンプレートが未定義なので、空のページが表示されています。

また「Log out」をクリックすることで、ログアウトを行うことができます。

## メイン

Drawerメニュー用のコンポーネントとして、``ClosableDrawer.jsx``を新規作成します。これを、``Header.jsx``にインポートすることで、Drawerメニューを実装します。

Drawerメニューを実装のためは、Material-UIコンポーネントの``<Drawer>``を使用します。``<Drawer>``の開閉を制御するために、親コンポーネントである``Header.jsx``内において、

1. open(state): Drawerの開閉を決める値(boolean)
2. handleDrawerToggle(コールバック関数): open の値を反転する関数

の二つを定義し、``ClosableDrawer.jsx``に対してpropsとして渡します。

```:実装ファイル
1.src/components/Header/Header.jsx
2.src/components/Header/HeaderMenus.jsx
3.src/components/Header/ClosableDrawer.jsx
4.src/components/Header/index.js
```


```jsx:1.src/components/Header/Header.jsx
import React,{useCallback,useState} from 'react';
.
.
.
import {HeaderMenus, ClosableDrawer} from "./index" {*追記*}
.
.
.
const Header = () => {
  .
  .
  .
  {*追記*}
  const [open, setOpen] = useState(false);

  const handleDrawerToggle = useCallback((event)=>{
    if (event.type === "keydown" && (event.key === "Tab" || event.key === "Shift")){
      return;
    }
    setOpen(!open)
  },[setOpen, open]);
  {*追記ここまで*}

  return (
    <div className={classes.root}>
      <AppBar position="fixed" className={classes.menuBar}>
        <Toolbar className={classes.toolBar}>
          <img
            src={logo} alt="Torahack Logo" width="128px"
            onClick={()=>dispatch(push("/"))}
          />
          {isSignedIn && (
            <div className={classes.iconButtons}>
              <HeaderMenus handleDrawerToggle={handleDrawerToggle}/> {*追記*}
            </div>
          )}
        </Toolbar>
      </AppBar>
      <ClosableDrawer open={open} onClose={handleDrawerToggle}/> {*追記*}
    </div>
  )
}

export default Header
```

- Drawerの開閉を決めるstateの``open``をuseStateで定義します。
- ``open``の論理値を反転させる``handleDrawerToggle()``を定義します。``event.type === "keydown" && ...``は、「TabキーとShiftキーをクリックした時はDrawerメニューを閉じない」ことを制御する条件文です。
- ``<HeaderMenus/>``へ``handleDrawerToggle()``を渡します。``<HeaderMenus/>``をクリックしたときにこの関数が動作する(Drawerメニューが開く)よう実装します。
- これから実装する``<ClosableDrawer/>``へ、``open``と``handleDrawerToggle ``を渡します。これにより、Drawerメニューの開閉自体を制御します。


```jsx:2.src/components/Header/HeaderMenus.jsx
.
.
.
const HeaderMenu = (props) => {
  return (
    <>
      .
      .
      .
      <IconButton>
        <MenuIcon onClick={(event) => props.handleDrawerToggle(event)}/> {*追記*}
      </IconButton>
    </>
  )
}

export default HeaderMenu
```

- MenuIconをクリックした時に、Drawerメニューが開くよう、onClickイベントに``handleDrawerToggle()``を設置します。

```jsx:3.src/components/Header/ClosableDrawer.jsx
import React, {useCallback, useState} from "react";
import Divider from "@material-ui/core/Divider";
import Drawer from "@material-ui/core/Drawer";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import {makeStyles} from "@material-ui/styles";
import IconButton from "@material-ui/core/IconButton";
import SearchIcon from "@material-ui/icons/Search";
import AddCircleIcon from "@material-ui/icons/AddCircle";
import HistoryIcon from "@material-ui/icons/History";
import PersonIcon from "@material-ui/icons/Person";
import ExitToAppIcon from "@material-ui/icons/ExitToApp"
import {TextInput} from "../UIkit";
import { useDispatch } from "react-redux";
import { push } from "connected-react-router"
import {signOut} from "../../reducks/users/operations"

const useStyles = makeStyles((theme)=>({
  drawer: {
    [theme.breakpoints.up("sm")]: {
      flexShrink: 0,
      width: 256
    }
  },
  toolbar: theme.mixins.toolbar,
  drawerPaper: {
    width: 256
  },
  searchField: {
    alignItems: "center",
    display: "flex",
    marginLeft: 32
  }
}));

const ClosableDrawer = (props) => {
  const classes = useStyles();
  const {container} = props;
  const dispatch = useDispatch();

  const [keyword, setKeyword] = useState("");

  const inputKeyword = useCallback((event)=>{
    setKeyword(event.target.value)
  },[setKeyword]);

  const selectMenu = (event, path) => {
    dispatch(push(path));
    props.onClose(event)
  }

  const handleSignOut = (event) => {
    dispatch(signOut());
    props.onClose(event)
  }

  const menus = [
    {func: selectMenu, label: "商品登録", icon: <AddCircleIcon/>, id: "register", value: "/product/edit"},
    {func: selectMenu, label: "注文履歴", icon: <HistoryIcon/>, id: "history", value: "/order/history"},
    {func: selectMenu, label: "プロフィール", icon: <PersonIcon/>, id: "profile", value: "/user/mypage"},
  ];

  return (
    <nav className={classes.drawer}>
      <Drawer
        container={container}
        variant="temporary"
        anchor="right"
        open={props.open}
        onClose={(e) => props.onClose(e)}
        classes={{paper: classes.drawerPaper}}
        ModalProps={{keepMounted: true}}
      >
        <div>
          <div className={classes.searchField}>
            <TextInput
              fullWidth={false} label={"キーワードを入力"} multiline={false}
              onChange={inputKeyword} required={false} rows={1} value={keyword} type={"text"}
            />
            <IconButton>
              <SearchIcon/>
            </IconButton>
          </div>
          <Divider />
          <List>
            {menus.map(menu => (
              <ListItem button key={menu.id} onClick={(e)=>menu.func(e, menu.value)}>
                <ListItemIcon>
                  {menu.icon}
                </ListItemIcon>
                <ListItemText primary={menu.label}/>
              </ListItem>
            ))}
            <ListItem button key="logout" onClick={(e) => handleSignOut(e)}>
              <ListItemIcon>
                <ExitToAppIcon/>
              </ListItemIcon>
              <ListItemText primary={"Log out"} />
            </ListItem>
          </List>
        </div>
      </Drawer>
    </nav>
  )
}

export default ClosableDrawer
```

今回の肝！全体の構造はざっくりこんな感じになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/11f6528a-5541-b385-ce96-a74b3aabfd11.png)

``<TextInput>``はUIKit内で自前で用意したコンポーネントで、これで検索窓を表現しています（実際の検索機能はまだありません）。それ以外はMaterial-UIから引っ張ってきています。

Materail-UI の ``<Drawer>``コンポーネントでDrawerメニューを実装する時は、``<nav>``タグで全体をラッピングしてコンポーネントを使用します。

```jsx:Drawerの実装
<nav>
  <Drawer>
  .
  .
  .
  </Drawer>
</nav>
```

親コンポーネントから渡ってきた``open``および``onClose(= handleDrawerToggle)``をこの``<Drawer/>``に渡すことで、Drawerメニューの開閉を実装できます。また、Drawer APIに渡すパラメーターで、Drawerメニューの特徴を設定できます。([公式リファレンス](https://material-ui.com/api/drawer/))

続いて、Drawerメニューの中身は``<List>``および``<ListItem>``で実装しています。各``<ListItem>``は、

- 表示するメニューアイコン
- 表示するラベル(文字列)
- クリックしたときに遷移する先のページのpath

の情報を持つ必要があります。

``<ListItem>``の中に直接記述することも可能ですが、今回は、``const menus = ...``でこれらを連想配列として定義しておき、``map``メソッドを用いて各``<ListItem>``に値を渡しています。

このように、 各``<ListItem>``が保有すべき情報をJSX外部に定義する書き方をすることで、コードの見通しがよくなるだけでなく、仮にメニュー数を増減したいときにも、JSX側は変更せずに``menus``の中身を書き換えるだけでよくなるため、保守性も高まります。

ただし、「ログアウト」の``<ListItem>``だけは、クリック時の動作が単純なページ遷移ではなく、「signOutアクションの実行によるサインアウト処理」となるため、別個で記述しています。

講座動画本編では、この``<ListItem>``のonClickイベントして直接``signOut()``を渡していました。しかし、その実装だとサインアウト後でも``open``の値がfalseにならず、Drawerメニューが開いたまま``/signin``へリダイレクトしてしまいます。

そのため動画本編にはありませんが、個人的に``handleSignOut()``関数を定義しています。サインアウト後に``props.onClose(event)``が走り、Drawerメニューが閉じるようになっています。

```js:4.src/components/Header/index.js
export {default as ClosableDrawer} from "./ClosableDrawer" //追記
export {default as Header} from "./Header"
export {default as HeaderMenus} from "./HeaderMenus"

```

- エントリーポイントに追加します。

###　動作確認

「完成系イメージ」と同じですがもう一度表示します。

**``http://localhost:3000``**

![10-2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/eae91a80-52da-15ad-d647-3fb6292fcc62.gif)

一見すると複雑なコーディングが必要そうなDrawerメニューですが、Material-UIを用いることで非常に少ないコード量かつ直感的なコーディングで実装ができました！Material-UI様様ですね

## さいごに
今回の要点をおさらいすると、

- Material-UIの``<Drawer>``コンポーネントを用いて、ネイティブアプリ風のDrawer Headerを作成できる。
- Drawerメニューの実装には、開閉の状態を決定する state と、それを制御する関数を定義して``<Drawer>``コンポーネントに渡す。

以上です！前回に引き続き、Material-UI の便利さを痛感する回でした。

このような学習内容を日々呟いていますので、よろしければTwitter([@ddpmntcpbr](https://twitter.com/ddpmntcpbr))のフォローもよろしくお願いします。