LoginSignup
2
0

More than 1 year has passed since last update.

【NextJS×MUI】Layoutにスナックバーを組み込む

Last updated at Posted at 2022-06-26

はじめに

NextJS × MUI でwebアプリを作成していて、ページ遷移後にスナックバーを表示したくなりました。

MUIのsnackbarはドキュメントを読む限りではDOM上に組み込んでおく必要があり、遷移後のページに組み込んでおいて遷移したことを検知して表示させるといったロジックが必要になってしまいます。
しかしこれでは遷移後のページが別ページのロジックを持つことになってしまい、嬉しくありません。
Angular MaterialのMatSnackBarAngular ionicのion-toastのように、関数を一つ叩くだけでページ関係なしに表示されて欲しくなります。

そこで、関数を叩けば下部にsnackbarを表示できるようにしてみました!

環境

実装方針

上記を達成するために以下の方針を立てました。

  • NextJSのLayout機能を用いて、表示したい部分にSnackbarを配置します
  • 表示する・しないのステータスはreduxで管理
  • reduxのステータスを書き換えるグローバル関数を定義

実装

方針を元に実装していきます。

_app.tsx

import Layout from 'path/to/components/layout'
import { store } from 'path/to/store';

export default function MyApp({ Component, pageProps }) {
  return (
    <Provider store={store}>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </Provider>
  );
}

layout.tsx

import { ReactElement } from 'react'
import { useSelector } from 'react-redux';
import Container from '@mui/material/Container';

import { OpenSnackBar } from 'path/to/snackbar';
import { RootStateType } from 'path/to/store';

type LayoutProps = Required<{
    readonly children: ReactElement
}>;

const Layout = ({ children }: LayoutProps) => {
    const selector = useSelector((state: RootStateType) => state.snackbar);
    return (
        <>
            <Container maxWidth="lg" component="main">
                {children}
            </Container>
            <OpenSnackBar
                open={selector.isOpen}
                alertType={selector.alertType}
                message={selector.message}
            ></OpenSnackBar>
        </>
    );
};

export default Layout;
  • メインコンテンツの入れ子としてContainerを用いています。

snackbar.tsx

import Alert from '@mui/material/Alert';
import Snackbar from '@mui/material/Snackbar';

type snackbarType = 'error' | 'warning' | 'info' | 'success';

/** propsの型 */
interface PropType {
    /** スナックバーを開くかどうか */
    open: boolean;
    /** アラートの種類 */
    alertType: snackbarType;
    /** スナックバーに表示するメッセージ */
    message?: string;
}

export const OpenSnackBar = ({ open, alertType, message }: PropType) => {
    return (
        <Snackbar
            anchorOrigin={{
                vertical: 'bottom',
                horizontal: 'center',
            }}
            open={open}
            key={alertType}
        >
            <Alert severity={alertType}>{ message ?? alertType }</Alert>
        </Snackbar>
    );
};
  • 下部真ん中に出るようにしてあります。
  • Alertのタイプでスナックバーを出せるようにしています

store.ts

import { configureStore, createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit'

type snackbarType = 'error' | 'warning' | 'info' | 'success';

type SnackbarStateType = {
    isOpen: boolean;
    alertType: snackbarType;
    message?: string;
};

export interface RootStateType {
    snackbar: SnackbarStateType;
}

const initialState: SnackbarStateType = {
    isOpen: false,
    alertType: 'success',
};

export const snackbarSlice = createSlice({
    name: "snackbar",
    initialState,
    reducers: {
        open(state, action: PayloadAction<{ alertType: snackbarType, message?: string }>) {
            state.isOpen = true;
            state.alertType = action.payload.alertType;
            state.message = action.payload.message;
            // 1.5秒後にスナックバーを閉じる
            setTimeout(() => {
                store.dispatch(snackbarSlice.actions.close());
            }, 1500);
        },
        close(state) {
            state.isOpen = false;
            state.message = undefined;
        }
    },
});

export const store = configureStore({
    reducer: {
        snackbar: snackbarSlice.reducer,
    },
});

service.ts

import { store, snackbarSlice } from 'path/to/store';

type snackbarType = 'error' | 'warning' | 'info' | 'success';

/**
 * スナックバーを表示させる
 * @param alertType スナックバーの種類
 * @param message 表示したいメッセージ
 */
export function openSnackbar(alertType: snackbarType, message?: string) {
    store.dispatch(snackbarSlice.actions.open({ alertType, message }));
}

この openSnackbar 関数を叩くことでstore内のスナックバー状態が変更され、与えた引数 alertType に応じたスナックバーが下部真ん中に現れます。

usage

import type { NextPage } from 'next';
import Button from '@mui/material/Button';
import { openSnackbar } from 'path/to/service';

const TestPage: NextPage = () => {
  /** successスナックバーを表示 */
  const onOpenSnackBarButton = () => {
    openSnackbar('success');
  };
  return (
    <>
      <Button variant="contained" onClick={onOpenSnackBarButton}>スナックバーを表示</Button>
    </>
  );
};

おわりに

Reduxが絡んでややこくなりますが、Angular MaterialのMatSnackBarAngular ionicのion-toastのように、関数を一つ叩くだけでページ関係なしにスナックバーを表示できるようになりました!
MUIのSnackbar APIには autoHideDuration という指定時間後に勝手に閉じてくれるプロパティがあるため、できることなら setTimeout は使いたくなかったですがstore内のsnackbarを表示するかの isOpen がtrueのままになってしまうためやむを得なくsetTimeoutしています。
もっといいやり方があれば教えてください。終わり!

参考

2
0
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
2
0