How to create awaitable prompt as React Component
Tags: react, ui, typescript
Published: 2024-07-06 14:23:07

# 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dmowbcxuphuven5wxkra.gif)

TyepScript implementation.
```typescript
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}/>
    )
  }
}
```

Style in SASS
```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;
}
```

How to use
```typescript
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}
    </>
  )
}
```

You can check [my git repo](https://github.com/ku6ryo/ReactPrompt) if you want. Hope this helps!