1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-13

はじめに

概要

この記事は、トラハック氏(@torahack_)が運営するYoutubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux講座 実践編』の学習備忘録です。

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

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

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

動画URL

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

要点

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

完成系イメージ

http://localhost:3000

10-2.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
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/>へ、openhandleDrawerToggle を渡します。これにより、Drawerメニューの開閉自体を制御します。
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()を設置します。
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

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

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

Drawerの実装
<nav>
  <Drawer>
  .
  .
  .
  </Drawer>
</nav>

親コンポーネントから渡ってきたopenおよびonClose(= handleDrawerToggle)をこの<Drawer/>に渡すことで、Drawerメニューの開閉を実装できます。また、Drawer 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メニューが閉じるようになっています。

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

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

さいごに

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

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

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

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

1
4
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
1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?