Forked from alexanderson1993/AlertDialogProvider.tsx
Last active
November 11, 2025 01:00
-
-
Save composite/f5785ab7be0a317dbb88f32d72ca3e5c to your computer and use it in GitHub Desktop.
A multi-purpose alert/confirm/prompt replacement built with shadcn/ui AlertDialog components. No Context, SSR friendly, Also works on Next.js and Remix, requires React 18 or later.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // For Next.js App Router usage | |
| import { type ReactNode } from 'react'; | |
| import OneDialog from '@/components/OneDialog'; | |
| export default function Layout({ | |
| children, | |
| }: Readonly<{ | |
| children: ReactNode; | |
| }>) { | |
| return ( | |
| <html lang="ko" className="dark m-0 h-full w-full"> | |
| <head> | |
| </head> | |
| <body> | |
| {children} | |
| <OneDialog /> | |
| </body> | |
| </html> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 'use client'; | |
| import * as React from 'react'; | |
| export type AlertType = 'alert' | 'confirm' | 'prompt'; | |
| export type AlertResult<T extends AlertType> = T extends 'prompt' | |
| ? string | false | |
| : T extends 'confirm' | |
| ? boolean | |
| : true; | |
| export type AlertRequest<T extends AlertType> = T extends 'alert' | |
| ? { | |
| /** | |
| * 경고창 제목 | |
| */ | |
| title?: React.ReactNode; | |
| /** | |
| * 경고창 내용 | |
| */ | |
| body: React.ReactNode; | |
| /** | |
| * 경고창 닫기 내용 | |
| */ | |
| closeButton?: React.ReactNode; | |
| } | |
| : T extends 'confirm' | |
| ? { | |
| /** | |
| * 질문창 제목 | |
| */ | |
| title?: React.ReactNode; | |
| /** | |
| * 질문창 내용 | |
| */ | |
| body: React.ReactNode; | |
| /** | |
| * 질문창 취소 내용 | |
| */ | |
| closeButton?: React.ReactNode; | |
| /** | |
| * 질문창 확인 내용 | |
| */ | |
| actionButton?: React.ReactNode; | |
| } | |
| : T extends 'prompt' | |
| ? { | |
| /** | |
| * 입력창 제목 | |
| */ | |
| title?: React.ReactNode; | |
| /** | |
| * 입력창 내용 | |
| */ | |
| body: React.ReactNode; | |
| /** | |
| * 입력창 취소 내용 | |
| */ | |
| closeButton?: React.ReactNode; | |
| /** | |
| * 입력창 확인 내용 | |
| */ | |
| actionButton?: React.ReactNode; | |
| /** | |
| * 입력창 기본값 | |
| */ | |
| defaultValue?: string; | |
| } | |
| : never; | |
| interface AlertDialogState<T extends AlertType> { | |
| title?: React.ReactNode; | |
| body: React.ReactNode; | |
| type: T; | |
| closeButton: React.ReactNode; | |
| actionButton: React.ReactNode; | |
| defaultValue?: string; | |
| resolver?: (value: AlertResult<T> | PromiseLike<AlertResult<T>>) => void; | |
| } | |
| const listeners: Array<(state: typeof memoryState) => void> = []; | |
| let memoryState: { dialog: AlertDialogState<AlertType>; open?: true } = | |
| {} as typeof memoryState; | |
| const dispatch = <T extends AlertType>(dialog?: AlertDialogState<T>) => { | |
| if (memoryState.dialog) { | |
| const { dialog } = memoryState; | |
| if (dialog?.resolver) { | |
| dialog.resolver((dialog.type === 'alert') as unknown as AlertResult<T>); | |
| delete dialog.resolver; | |
| } | |
| } | |
| memoryState = dialog | |
| ? { dialog, open: true } | |
| : { dialog: memoryState.dialog }; | |
| listeners.forEach((listener) => listener(memoryState)); | |
| }; | |
| const promiser = <T>() => { | |
| let resolve: unknown, reject: unknown; | |
| const promise = new Promise<T>((ok, no) => { | |
| resolve = ok; | |
| reject = no; | |
| }); | |
| return { | |
| promise, | |
| resolve: resolve as (value: T | PromiseLike<T>) => void, | |
| reject: reject as (reason?: any) => void, | |
| }; | |
| }; | |
| function subscribe(listener: () => void) { | |
| listeners.push(listener); | |
| return () => { | |
| const i = listeners.indexOf(listener); | |
| if (i > -1) { | |
| listeners.splice(i, 1); | |
| } | |
| }; | |
| } | |
| function getSnapshot() { | |
| return memoryState; | |
| } | |
| /** | |
| * OneDialog 호스트용 상태 관리 | |
| */ | |
| export function useOneDialog() { | |
| const state = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot); | |
| return { ...state, dispatch }; | |
| } | |
| /** | |
| * 비동기 경고창 | |
| * @param params 경고창 속성 | |
| */ | |
| export function alert(params: AlertRequest<'alert'>) { | |
| const { promise, resolve } = promiser<AlertResult<'alert'>>(); | |
| const o: AlertDialogState<'alert'> = { | |
| ...params, | |
| type: 'alert', | |
| closeButton: params.closeButton ?? 'Close', | |
| actionButton: '', | |
| resolver: (a) => { | |
| resolve(a); | |
| delete o.resolver; | |
| }, | |
| }; | |
| dispatch(o); | |
| return promise; | |
| } | |
| /** | |
| * 비동기 질문창 | |
| * @param params 질문창 속성 | |
| */ | |
| export function confirm(params: AlertRequest<'confirm'>) { | |
| const { promise, resolve } = promiser<AlertResult<'confirm'>>(); | |
| const o: AlertDialogState<'confirm'> = { | |
| ...params, | |
| type: 'confirm', | |
| closeButton: params.closeButton ?? 'Close', | |
| actionButton: params.actionButton ?? 'Confirm', | |
| resolver: (a) => { | |
| resolve(a); | |
| delete o.resolver; | |
| }, | |
| }; | |
| dispatch(o); | |
| return promise; | |
| } | |
| /** | |
| * 비동기 입력창 | |
| * @param params 입력창 속성 | |
| */ | |
| export function prompt(params: AlertRequest<'prompt'>) { | |
| const { promise, resolve } = promiser<AlertResult<'prompt'>>(); | |
| const o: AlertDialogState<'prompt'> = { | |
| ...params, | |
| type: 'prompt', | |
| closeButton: params.closeButton ?? 'Close', | |
| actionButton: params.actionButton ?? 'Confirm', | |
| resolver: (a) => { | |
| resolve(a); | |
| delete o.resolver; | |
| }, | |
| }; | |
| dispatch(o); | |
| return promise; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 'use client'; | |
| import { | |
| AlertDialog, | |
| AlertDialogContent, | |
| AlertDialogDescription, | |
| AlertDialogFooter, | |
| AlertDialogHeader, | |
| AlertDialogTitle, | |
| } from '@/components/ui/alert-dialog'; | |
| import { Input } from '@/components/ui/input'; | |
| import { Button } from '@/components/ui/button'; | |
| import { | |
| type AlertResult, | |
| type AlertType, | |
| useOneDialog, | |
| } from './one-dialog'; | |
| export default function OneDialog() { | |
| const { dialog, open, dispatch } = useOneDialog(); | |
| const handleClose = (result?: AlertResult<AlertType>) => { | |
| if (dialog) { | |
| const value: unknown = dialog.type === 'alert' ? true : (result ?? false); | |
| if (dialog.resolver) { | |
| dialog.resolver(value as AlertResult<AlertType>); | |
| } | |
| dispatch(); | |
| } | |
| }; | |
| return ( | |
| <AlertDialog | |
| open={!!open} | |
| onOpenChange={(opened: boolean) => { | |
| if (!opened) handleClose(); | |
| }} | |
| > | |
| <AlertDialogContent | |
| asChild | |
| onFocusOutside={(e: Event) => e.preventDefault()} | |
| > | |
| <form | |
| onSubmit={(event) => { | |
| event.preventDefault(); | |
| handleClose( | |
| dialog?.type === 'prompt' | |
| ? event.currentTarget.prompt?.value || '' | |
| : true | |
| ); | |
| }} | |
| > | |
| <AlertDialogHeader> | |
| <AlertDialogTitle>{dialog?.title ?? ''}</AlertDialogTitle> | |
| {dialog?.body && ( | |
| <AlertDialogDescription>{dialog?.body}</AlertDialogDescription> | |
| )} | |
| </AlertDialogHeader> | |
| {dialog?.type === 'prompt' && ( | |
| <Input name="prompt" defaultValue={dialog?.defaultValue} /> | |
| )} | |
| <AlertDialogFooter> | |
| {dialog?.type !== 'alert' && ( | |
| <Button type="submit" className="min-w-20">{dialog?.actionButton}</Button> | |
| )} | |
| <Button | |
| type="button" | |
| variant={dialog?.type === 'alert' ? 'default' : 'outline'} | |
| className="min-w-20" | |
| onClick={() => handleClose()} | |
| > | |
| {dialog?.closeButton} | |
| </Button> | |
| </AlertDialogFooter> | |
| </form> | |
| </AlertDialogContent> | |
| </AlertDialog> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // For Remix usage | |
| import { | |
| Links, | |
| LiveReload, | |
| Meta, | |
| Outlet, | |
| Scripts, | |
| ScrollRestoration, | |
| } from "@remix-run/react"; | |
| import OneDialog from '@/components/OneDialog'; | |
| export default function App() { | |
| return ( | |
| <html lang="en"> | |
| <head> | |
| <meta charSet="utf-8" /> | |
| <meta | |
| name="viewport" | |
| content="width=device-width, initial-scale=1" | |
| /> | |
| <Meta /> | |
| <Links /> | |
| </head> | |
| <body> | |
| <Outlet /> | |
| <ScrollRestoration /> | |
| <Scripts /> | |
| <LiveReload /> | |
| <OneDialog /> | |
| </body> | |
| </html> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { | |
| alert, | |
| confirm, | |
| prompt, | |
| } from "@/components/one-dialog"; | |
| import { Button } from "@/components/ui/Button"; | |
| export default function Test() { | |
| return ( | |
| <> | |
| <Button | |
| onClick={async () => { | |
| console.log( | |
| await alert({ | |
| title: "Test", | |
| body: "Just wanted to say you're cool.", | |
| cancelButton: "Heyo!", | |
| }) // false | |
| ); | |
| }} | |
| type="button" | |
| > | |
| Test Alert | |
| </Button> | |
| <Button | |
| onClick={async () => { | |
| console.log( | |
| await confirm({ | |
| title: "Test", | |
| body: "Are you sure you want to do that?", | |
| cancelButton: "On second thought...", | |
| }) // true | false | |
| ); | |
| }} | |
| type="button" | |
| > | |
| Test Confirm | |
| </Button> | |
| <Button | |
| onClick={async () => { | |
| console.log( | |
| await prompt({ | |
| title: "Test", | |
| body: "Hey there! This is a test.", | |
| defaultValue: "Something something" + Math.random().toString(), | |
| }) // string | false | |
| ); | |
| }} | |
| type="button" | |
| > | |
| Test Prompt | |
| </Button> | |
| </> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment