はじめに
NextJS × MUI でwebアプリを作成していて、ページ遷移後にスナックバーを表示したくなりました。
MUIのsnackbarはドキュメントを読む限りではDOM上に組み込んでおく必要があり、遷移後のページに組み込んでおいて遷移したことを検知して表示させるといったロジックが必要になってしまいます。
しかしこれでは遷移後のページが別ページのロジックを持つことになってしまい、嬉しくありません。
Angular MaterialのMatSnackBarやAngular ionicのion-toastのように、関数を一つ叩くだけでページ関係なしに表示されて欲しくなります。
そこで、関数を叩けば下部にsnackbarを表示できるようにしてみました!
環境
- TypeScript
- NextJS
- MUI
- Redux Toolkit
- React 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のMatSnackBarやAngular ionicのion-toastのように、関数を一つ叩くだけでページ関係なしにスナックバーを表示できるようになりました!
MUIのSnackbar APIには autoHideDuration
という指定時間後に勝手に閉じてくれるプロパティがあるため、できることなら setTimeout
は使いたくなかったですがstore内のsnackbarを表示するかの isOpen
がtrueのままになってしまうためやむを得なくsetTimeoutしています。
もっといいやり方があれば教えてください。終わり!