DEV Community

Ryo Kuroyanagi
Ryo Kuroyanagi

Posted on

How to create awaitable prompt as React Component

Motivation

JavaScript browser API has prompt() function which is a synchronized function for getting text input from user. We sometimes uses that kind of input UI components. However, the natively implemented UI component cannot be customized. I wanted to make it with customized UI and make it awaitable like const value = await prompt();.

Implementation

Like public react component libraries, I implemented use hook function. I'm exposing only the usePrompt() because I do not want developers to care about the UI implementation and want them to focus on using it as a capsulized feature.

Image description

TyepScript implementation.

 import styles from "./style.module.scss" import { useState, useRef, useCallback } from "react" import { createPortal } from "react-dom" type Props = { open: boolean value: string onChange: (value: string) => void onClose: (value: string | null) => void } export function Prompt({ open, value, onChange: onValueChange, onClose }: Props) { const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { onValueChange(e.target.value) }, [onValueChange]) const onOkClick = useCallback(() => { onClose(value) }, [value, onClose]) const onCancelClick = useCallback(() => { onClose(null) }, [onClose]) return createPortal(( open && ( <div className={styles.cover}> <div className={styles.frame}> <div> <input type="text" value={value} onChange={onChange} />  </div>  <div> <button onClick={onCancelClick}>CANCEL</button>  <button onClick={onOkClick}>OK</button>  </div>  </div>  </div>  ) ), document.body) } export function usePrompt() { const [open, setOpen] = useState<boolean>(false) const [value, setValue] = useState<string>("") const onCloseRef = useRef<(value: string | null) => void>() const onClose = useCallback((value: string | null) => { setOpen(false) if (onCloseRef.current) { onCloseRef.current(value) } }, [setOpen, onCloseRef]) const onChange = (value: string) => { setValue(value) } return { open: async (value: string) => { setOpen(true) setValue(value) return new Promise<string|null>((resolve) => { onCloseRef.current = (value: string | null) => { resolve(value) } }) }, elem: ( <Prompt open={open} value={value} onClose={onClose} onChange={onChange}/>  ) } } 
Enter fullscreen mode Exit fullscreen mode

Style in SASS

 .cover { align-items: center; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; height: 100dvh; left: 0; position: fixed; top: 0; width: 100dvw; } .frame { background-color: white; padding: 16px; } 
Enter fullscreen mode Exit fullscreen mode

How to use

 import { usePrompt } from "./Prompt" import { useState } from "react" function App() { const { open, elem } = usePrompt() const [value, setValue] = useState<string>("") const onOpenClick = () => { open("Initial value").then((value) => { setValue(value || "cancelled") }) } return ( <> <div> <button onClick={onOpenClick}>Open prompt</button>  </div>  {value && <div>Input value: {value}</div>}  {elem} </>  ) } 
Enter fullscreen mode Exit fullscreen mode

You can check my git repo if you want. Hope this helps!

Top comments (0)