はじめに
概要
この記事は、トラハック氏(@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
ヘッダーのメニューアイコンをクリックすることで、Drawer Menuを表示させられます。
キーワード欄はダミーで、今はただのテキストフィールドです。最終的にはここで商品情報の検索を行えるようにします。
メニューアイテムの「商品情報」「注文履歴」「プロフィール」をクリックすることで、対応するページへ遷移します。今は「注文履歴」「プロフィール」についてはテンプレートが未定義なので、空のページが表示されています。
また「Log out」をクリックすることで、ログアウトを行うことができます。
メイン
Drawerメニュー用のコンポーネントとして、ClosableDrawer.jsx
を新規作成します。これを、Header.jsx
にインポートすることで、Drawerメニューを実装します。
Drawerメニューを実装のためは、Material-UIコンポーネントの<Drawer>
を使用します。<Drawer>
の開閉を制御するために、親コンポーネントであるHeader.jsx
内において、
- open(state): Drawerの開閉を決める値(boolean)
- 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
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メニューの開閉自体を制御します。
.
.
.
const HeaderMenu = (props) => {
return (
<>
.
.
.
<IconButton>
<MenuIcon onClick={(event) => props.handleDrawerToggle(event)}/> {*追記*}
</IconButton>
</>
)
}
export default HeaderMenu
- MenuIconをクリックした時に、Drawerメニューが開くよう、onClickイベントに
handleDrawerToggle()
を設置します。
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
今回の肝!全体の構造はざっくりこんな感じになっています。
<TextInput>
はUIKit内で自前で用意したコンポーネントで、これで検索窓を表現しています(実際の検索機能はまだありません)。それ以外はMaterial-UIから引っ張ってきています。
Materail-UI の <Drawer>
コンポーネントでDrawerメニューを実装する時は、<nav>
タグで全体をラッピングしてコンポーネントを使用します。
<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メニューが閉じるようになっています。
export {default as ClosableDrawer} from "./ClosableDrawer" //追記
export {default as Header} from "./Header"
export {default as HeaderMenus} from "./HeaderMenus"
- エントリーポイントに追加します。
### 動作確認
「完成系イメージ」と同じですがもう一度表示します。
http://localhost:3000
一見すると複雑なコーディングが必要そうなDrawerメニューですが、Material-UIを用いることで非常に少ないコード量かつ直感的なコーディングで実装ができました!Material-UI様様ですね
さいごに
今回の要点をおさらいすると、
- Material-UIの
<Drawer>
コンポーネントを用いて、ネイティブアプリ風のDrawer Headerを作成できる。 - Drawerメニューの実装には、開閉の状態を決定する state と、それを制御する関数を定義して
<Drawer>
コンポーネントに渡す。
以上です!前回に引き続き、Material-UI の便利さを痛感する回でした。
このような学習内容を日々呟いていますので、よろしければTwitter(@ddpmntcpbr)のフォローもよろしくお願いします。