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!

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay