【React × Typescript】サイドバーの開閉をuseContextで状態管理する


## 事前準備
[こちら](https://qiita.com/camecamel/items/5100da10b386f4b66ba6)の記事を参考にヘッダーまで作成してみましょう！
以下が作成した画面です。（ダークモード設定時）
![スクリーンショット 2023-02-04 16.54.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115156/84ca0568-11e1-8df4-b2fe-4c9492516eb1.png)

## ヘッダー部分とメニューボタンをコンポーネント化
### ヘッダーのコンポーネント化
ヘッダーをコンポーネント化してトップ画面のコードを整理します。
```typescript:src/components/pages/top/top.tsx
import * as React from 'react';
import { Header } from '../../model/header/header';

export const Top = () => {
  return (
    <>
      <Header />
    </>
  );
}
```
ヘッダーコンポーネントのコードは以下の通りです。
```typescript:src/components/model/header/header.tsx
import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';

export const Header = () => {
  return (
    <Box sx={{ flexGrow: 1 }}>
      <AppBar position="static">
        <Toolbar>
          <IconButton
            size="large"
            edge="start"
            color="inherit"
            aria-label="menu"
            sx={{ mr: 2 }}
          >
            <MenuIcon />
          </IconButton>
          <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
            News
          </Typography>
          <Button color="inherit">Login</Button>
        </Toolbar>
      </AppBar>
    </Box>
  );
}
```
これでヘッダーのコンポーネント化は完了です。

### メニューボタンのコンポーネント化
メニューボタンをコンポーネント化して、ヘッダーのコードを整理します。
```typescript:src/components/ui/button/menuButton.tsx
import * as React from 'react';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';

export function MenuButton() {
    return (
        <>
            <IconButton
                size="large"
                edge="start"
                color="inherit"
                aria-label="menu"
                sx={{ mr: 2 }}
            >
                <MenuIcon />
            </IconButton>
        </>
    );
 }
```

```typescript:src/components/model/header/header.tsx
import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import { MenuButton } from '../../ui/button/menuButton';

export const Header = () => {
  return (
    <Box sx={{ flexGrow: 1 }}>
      <AppBar position="static">
        <Toolbar>
          <MenuButton />
          <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
            News
          </Typography>
          <Button color="inherit">Login</Button>
        </Toolbar>
      </AppBar>
    </Box>
  );
}
```
これでメニューボタンのコンポーネント化は完了です。

## サイドバー開閉の実装
### useContextについて
まずはサイドバー開閉の状態を管理する`useContext`について説明します。
`useContext` とは `Context` 機能をよりシンプルに使えるようになった `React` 独自の機能です。
簡単に言うと、親から `props` で渡されていないのに、`Context` に収容されているデータにアクセスできるというものです。

:::note info
`Context` とは
`props` とは別の方法でコンポーネントに動的に値を渡す、 `React` 独自のAPIです。
`props` のバケツリレーを回避するために存在します。
:::

### useContextの実装
`createContext` メソッドを利用し、サイドバー開閉の状態を管理する `menuContext` を作成します。
```typescript:src/hooks/appState.ts
import { createContext } from "react";

export const menuContext = createContext({
    isOpened: false,
    setOpened: (isOpen: boolean) => {}
});
```
### menuContextを参照する
上記で作成した `menuContext` を参照します。
```typescript:src/components/ui/button/menuButton.tsx
import * as React from 'react';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import { useContext } from 'react';
import { menuContext } from '../../../hooks/appState';

export function MenuButton() {
    const { isOpened, setOpened } = useContext(menuContext);
    const toggleOpen = () => setOpened(!isOpened);

    return (
        <>
            <IconButton
                size="large"
                edge="start"
                color="inherit"
                aria-label="open drawer"
                onClick={toggleOpen}
                sx={{ mr: 2 }}
            >
                <MenuIcon />
            </IconButton>
        </>
    );
 }
```
`onClick` イベントを追加することでヘッダーにあるメニューボタンをクリックした際に　`toggleOpen` メソッドが呼び出され、`menuContext` の値が変化します。

### サイドバーの作成
開閉するサイドバーを作成します。
```typescript:src/components/model/drawer/drawer.tsx
import * as React from 'react';
import Box from '@mui/material/Box';
import Drawer from '@mui/material/Drawer';
import Toolbar from '@mui/material/Toolbar';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import { ListItemText } from '@mui/material';

type Props = {
    open: boolean;
};

export const DesktopDrawer: React.FC<Props> = (props) => {
    return (
        <>
            <Drawer
                sx={{
                    width: 200,
                    flexShrink: 0,
                    '& .MuiDrawer-paper': {
                        width: 200,
                    },
                }}
                variant="persistent"
                anchor="left"
                open={props.open}
            >
                <Toolbar />
                <Box sx={{overflow: "auto"}}>
                    <List>
                        <ListItemButton>
                        <ListItemText
                            sx={{ my: 0 }}
                            primary="home"
                            primaryTypographyProps={{
                                fontSize: 18,
                                fontWeight: 'medium',
                                letterSpacing: 0,
                            }}
                        >
                        </ListItemText>
                        <ListItem />
                        </ListItemButton>
                    </List>
                </Box>
            </Drawer>
        </>
    );
};
```
サイドバー側では、`drawer` の開閉状態をシンプルに `props` で受け取るようにします。

### トップ画面にサイドバー開閉を実装
トップ画面に作成したサイドバー開閉を反映させます。
```typescript:src/components/pages/top/top.tsx
import * as React from 'react';
import { Header } from '../../model/header/header';
import { menuContext } from '../../../hooks/appState';
import { DesktopDrawer } from '../../model/drawer/drawer';

export const Top = () => {
    const [isOpened, setOpened] = React.useState(true);

    return (
        <>
            <menuContext.Provider value={{isOpened, setOpened}}>
                    <Header />
                    <DesktopDrawer open={isOpened} />
            </menuContext.Provider>
        </>
    );
 }
```
トップ画面では、`menuContext` の `Provider`　を通じて `ReactContext` を利用できるようにします。
その上で、開閉状態を読み取るコンポーネントである `DesktopDrawer` には `isOpened` プロパティを渡します。

:::note info
`Provider` とは
全てのコンテクストオブジェクト(`Context`)にはプロバイダ(`Provider`)コンポーネントが付属しています。
`Provider`　コンポーネントは、`props` として `value` を受け取ります。
この `value`　に入れるのは、バケツリレーしていた値です。
:::

作成した画面は以下の通りになります。
![スクリーンショット 2023-02-04 18.11.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115156/711f2305-0fd5-78df-15dd-2357d03b2623.png)
あ…
ヘッダーが隠れてしまいました…
これは私もつまずいた所で、完成したと思いきやこの画面が出てきた時は絶望しました（笑）
でも安心して下さい（笑）解決したからこの記事を書いています。
ヘッダーコンポーネントを以下のように修正すれば解決します。
```typescript:src/components/model/header/header.tsx
import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import { MenuButton } from '../../ui/button/menuButton';

export const Header = () => {
  return (
    <Box sx={{ flexGrow: 1 }}>
        <AppBar
            position="fixed"
            sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
        >
        <Toolbar>
          <MenuButton />
          <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
            News
          </Typography>
          <Button color="inherit">Login</Button>
        </Toolbar>
      </AppBar>
    </Box>
  );
}
```
修正した箇所は、 `        <AppBar
            position="fixed"
            sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
        >`
です。
![drewer open.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115156/6b7624e0-662d-035c-207e-454484bc5403.gif)
これでヘッダーに隠れず、サイドバーの開閉が可能になりました！