
Snackbarコンポーネント
import { Fragment } from "react";
import { Transition } from "@headlessui/react";
import {
CheckCircleIcon,
ExclamationIcon,
InformationCircleIcon,
BanIcon,
} from "@heroicons/react/outline";
import { XIcon } from "@heroicons/react/solid";
interface Props {
open: boolean;
setOpen: (show: boolean) => void;
variant?: "success" | "error" | "info" | "warning" | undefined;
message?: string;
}
export function Snackbar({
open,
setOpen,
variant = "success",
message,
}: Props) {
const icon = () => {
switch (variant) {
case "success":
return <CheckCircleIcon className="h-6 w-6 text-green-400" />;
case "error":
return <BanIcon className="h-6 w-6 text-red-400" />;
case "info":
return <InformationCircleIcon className="h-6 w-6 text-blue-400" />;
case "warning":
return <ExclamationIcon className="h-6 w-6 text-orange-400" />;
default:
return <CheckCircleIcon className="h-6 w-6 text-green-400" />;
}
};
return (
<>
<div
aria-live="assertive"
className="z-[9999] fixed inset-0 flex items-end px-4 py-6 pointer-events-none sm:p-6 sm:items-start"
>
<div className="w-full flex flex-col items-center space-y-4 sm:items-end">
<Transition
show={open}
as={Fragment}
enter="transform ease-out duration-300 transition"
enterFrom="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
enterTo="translate-y-0 opacity-100 sm:translate-x-0"
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="max-w-sm bg-white w-full shadow-lg rounded-lg pointer-events-auto overflow-hidden">
<div className="p-4">
<div className="flex items-start">
<div className="flex-shrink-0">{icon()}</div>
<div className="ml-3 w-0 flex-1 pt-0.5">
<p className="text-sm font-medium">{message}</p>
</div>
<div className="ml-4 flex-shrink-0 flex self-center">
<button
type="button"
className="rounded-md inline-flex text-gray-800"
onClick={() => {
setOpen(false);
}}
>
<span className="sr-only">Close</span>
<XIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
</div>
</Transition>
</div>
</div>
</>
);
}
SnackbarContext
import React, { useState, createContext, useContext } from "react";
import { Snackbar } from "@/components/shared/Snackbar";
interface ContextValues {
openSnackbar: (
message: string,
variant: "success" | "error" | "info" | "warning"
) => void;
closeSnackbar: () => void;
showSuccess: (message: string, ms?: number) => void;
showError: (message: string, ms?: number) => void;
showWarning: (message: string, ms?: number) => void;
showInfo: (message: string, ms?: number) => void;
}
export const SnackbarContext = createContext<ContextValues>({
openSnackbar: () => console.error("No context."),
closeSnackbar: () => console.error("No context."),
showSuccess: () => console.error("No context."),
showError: () => console.error("No context."),
showWarning: () => console.error("No context."),
showInfo: () => console.error("No context."),
});
export const SnackbarProvider: React.FC = ({ children }) => {
const [open, setOpen] = useState(false);
const [message, setMessage] = useState("");
const [variant, setVariant] = useState<
"success" | "error" | "info" | "warning"
>("success");
// NOTE: 共通でSnackbarを削除する場合
// useEffect(() => {
// if (open) {
// setTimeout(() => {
// setOpen(false);
// }, 3000);
// }
// }, [open]);
const openSnackbar = (
message: string,
variant: "success" | "error" | "info" | "warning"
) => {
setMessage(message);
setVariant(variant);
setOpen(true);
};
const showSuccess = (message: string, ms?: number) => {
openSnackbar(message, "success");
if (ms) {
setTimeout(() => {
setOpen(false);
}, ms);
}
};
const showError = (message: string, ms?: number) => {
openSnackbar(message, "error");
if (ms) {
setTimeout(() => {
setOpen(false);
}, ms);
}
};
const showWarning = (message: string, ms?: number) => {
openSnackbar(message, "warning");
if (ms) {
setTimeout(() => {
setOpen(false);
}, ms);
}
};
const showInfo = (message: string, ms?: number) => {
openSnackbar(message, "info");
if (ms) {
setTimeout(() => {
setOpen(false);
}, ms);
}
};
const closeSnackbar = () => {
setOpen(false);
};
return (
<SnackbarContext.Provider
value={{
openSnackbar,
closeSnackbar,
showSuccess,
showError,
showWarning,
showInfo,
}}
>
{children}
<Snackbar
open={open}
setOpen={setOpen}
message={message}
variant={variant}
/>
</SnackbarContext.Provider>
);
};
export const useSnackbar = () => {
return useContext(SnackbarContext);
};
app.tsx
function MyApp({ Component, pageProps }: any) {
return (
<SnackbarProvider>
<Component {...pageProps} />
</SnackbarProvider>
);
}
API Call
自動的にSnackbarを非表示にする
import { useSnackbar } from "@/contexts/SnackbarContext";
const { showSuccess, closeSnackbar } = useSnackbar();
const handleClick = () => {
// 1秒後に自動的にSnackbarを非表示にする
showSuccess('メールアドレスを送信しました', 1000);
}
マニュアル
import { useSnackbar } from "@/contexts/SnackbarContext";
const { showSuccess, closeSnackbar } = useSnackbar();
const handleClick = () => {
showSuccess('メールアドレスを送信しました');
closeSnackbar();
}
注意点
useEffect
などで呼び出す際に無限ループしないように気をつける
- 呼び出し方によっては、
Warning: Cannot update a component Context...
が発生するので注意->コンポーネント内でshowError
などを呼び出す場合、呼び出し元のコンポーネントのレンダリングが完了していなければならない。SnackbarContext内でuseState
などのhooksを呼び出しているので、