nishimura.clubnishimura.club

ReactContextでSnackbarContextの作成と使い方

作成日
2022-06-04
更新日
2022-06-04

Snackbar

Snackbarコンポーネント

Snackbar.tsx
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

SnackbarContext.tsx
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

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を呼び出しているので、

Related