next.js(React)でポップアップメッセージを簡単に呼び出せるようにする

# はじめに
- React、next.jsでのWebアプリ開発者向けの内容（本記事の環境はnext.js/TypeScript）
- 何か操作した時の結果（成功、失敗、警告など）をポップアップメッセージで表示したい、そのための処理を簡単に書きたい人向け
- Material UIのSnackbarコンポーネントの描画処理を、React Contextでどのコンポーネントからでも簡単に呼び出せるようにする

# Snackbar（スナックバー）
- 短いメッセージをポップアップ表示する
- Material UIのコンポーネントで提供されており、severity（重要度）のpropsを指定すれば様々なスタイルのSnackbarを表示できる
  - https://mui.com/material-ui/react-snackbar/
![スクリーンショット 2022-07-07 22.07.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37298/c76bb1b0-4060-1a53-53d9-dd24e8d4dbe4.png)

# やりたいこと
- Webアプリ上で何かイベントが起きた際にsnackbarを表示したい
  - データの追加操作の成功時に成功メッセージを表示
  - 操作に失敗したらエラーメッセージを表示　など
- snackbarを表示したい全てのコンポーネントで以下のようなコードを書くのは**面倒、共通化したい**

```ts
const [open, setOpen] = React.useState(false);

  const handleClick = () => {
    setOpen(true);
  };

  const handleClose = (event?: React.SyntheticEvent | Event, reason?: string) => {
    if (reason === 'clickaway') {
      return;
    }

    setOpen(false);
  };

  return (
    <Stack spacing={2} sx={{ width: '100%' }}>
      <Button variant="outlined" onClick={handleClick}>
        Open success snackbar
      </Button>
      <Snackbar open={open} autoHideDuration={6000} onClose={handleClose}>
        <Alert onClose={handleClose} severity="success" sx={{ width: '100%' }}>
          This is a success message!
        </Alert>
      </Snackbar>
      <Alert severity="error">This is an error message!</Alert>
    </Stack>
  );
```

# React ContextでSnackbar表示関数をどこからでも呼び出せるようにする
## SnackbarProviderコンポーネントを作る
- 今回は表示する種類（severity)、表示するメッセージ（message）をイベントに応じて変化させたいので、contextに格納する関数 **showSnackbar**の引数にこの2つを指定。表示位置、表示秒数など他のパラメータを変えたければ引数を増やす。
```tsx:context-provider.ts
import React, { createContext, useState, useContext, FC } from 'react';
import Snackbar from "@mui/material/Snackbar";
import Alert from '@mui/material/Alert';

type SnackbarSeverity = "error" | "warning" | "info" | "success";

interface ISnackbarContext {
    //snackbarを表示するときに呼び出す関数
    showSnackbar: ((type: SnackbarSeverity, message: string) => void) | undefined;
  }

const SnackbarContext = createContext<ISnackbarContext>({showSnackbar: undefined});

export function useSnackbarContext() {
  return useContext(SnackbarContext);
}

export const SnackbarProvider: FC = ({ children }) => {
    //Snackbarに与えるパラメータをstateで管理
    const [open, setOpen] = useState<boolean>(false);
    const [severity, setSeverity] = useState<SnackbarSeverity>("info");
    const [message, setMessage] = useState<string>("");

    //showSnackbarの実体。各stateをセットし、snackbarを表示する
    const showSnackbar= (type: SnackbarSeverity, message: string): void => {
        setOpen(true);
        setSeverity(type);
        setMessage(message);
    };

    //snackbarのxボタンが押された時のコールバック関数
    const handleClose = (event: React.SyntheticEvent | Event, reason?: string) => {
        if (reason === 'clickaway') {
          return;
        }
    
        setOpen(false);
      };


    return (
        <React.Fragment>
            {/* 子コンポーネントの描画 */}
            <SnackbarContext.Provider value={{showSnackbar: showSnackbar}}>{children}</SnackbarContext.Provider>
            {/* Snackbarの描画 */}
            <Snackbar open={open} autoHideDuration={6000} onClose={handleClose}>
                <Alert onClose={handleClose} severity={severity} sx={{ width: '100%' }}>
                    {message}
                </Alert>
            </Snackbar>
        </React.Fragment>
        
    );
};
```

## SnackbarProviderでラップする
- 今回はNext.jsで開発している環境のため、_app.tsxでアプリのコンポーネント全体にラップする

```tsx:pages/_app.tsx
import { SnackbarProvider } from '../components/snackbar-provider';

const App = ({ Component, pageProps }: AppProps) => (
    <div>
        <Head>
            <meta
                name="viewport"
                content="width=device-width, initial-scale=1, shrink-to-fit=no"
            />
        </Head>
            <SnackbarProvider>
                <Component {...pageProps} />
            </SnackbarProvider>
    </div>
);

export default App
```

## showSnackbarを呼び出す
- 各コンポーネントでuseSnackbarContextをインポートし、Snackbarを表示したいタイミングでshowSnackbarを呼び出す。
- 以下はDBへのアイテム新規作成するボタンが押された場合に成功、失敗メッセージを表示する例
- context-provider.tsxのcreateContextを通すためにshowSnackbarの型にundefinedを許容しているため、呼び出し前にif文でshowSnackbarの存在確認が必要
```tsx: create-item.tsx
import { useSnackbarContext } from "../../components/snackbar-provider";

export const CreatePage: React.FC = () => {
    const { showSnackbar } = useSnackbarContext();
    const onCreateClick = (item: Item) => {
            addItem(item)
                .then(() => {
                    if(showSnackbar){
                        //成功のSnackbar表示
                        showSnackbar("success", "アイテムが新規作成されました")
                    }
                })
                .catch((error) => {
                    if(showSnackbar){
                        //エラーメッセージのSnackbar表示
                        showSnackbar("error", error.message)
                    }
        }
    };

    return (
        <div>
            <InputItem onSubmit={onCreateClick} />
        </div>
    );
};
```

# まとめ
- React ContextでSnackbar表示関数をどのコンポーネントからも簡単に呼び出せるようにした
- 表示位置、サイズ、自動非表示の有無などもshowSnackbarの引数で制御できるようにすれば、重要メッセージは画面中央、大きく、消えずに表示させるなど、汎用性を高めることもできそう