UNPKG

@prezly/react-promise-modal

Version:

The proper (and easy) way of doing modals in React. With Promises.

217 lines (164 loc) 6.57 kB
# React Promise Modal `usePromiseModal()` is a React hook that allows you to define a modal by providing a custom rendering function. After defining your modal you can invoke it imperatively and await the returned promise to resolve, to get the modal resolution result. ## Usage 1. Define your modal with `usePromiseModal()` 2. Invoke it from your event handler using `invoke()` 3. Wait for the modal to resolve with `await` ```js // 1) Define your modal const confirmation = usePromiseModal((props) => <MyModal {...props} />); // 2) Call it in your event handler async function handleClick() { // 3) Wait for the modal to resolve if (await confirmation.invoke()) { // TODO: Perform the operation. } } ``` **Demo: https://codesandbox.io/p/sandbox/romantic-lovelace-4pmm3k** ## API **The `usePromiseModal()` hook` returns the following values**: ```tsx const { invoke, modal, isDisplayed } = usePromiseModal(/* ... */); ``` - `invoke` — imperatively invoke the modal, optionally passing additional call-time arguments. Returns a promise you can _await_ to get the modal resolution value, when it's available. Or _undefined_ if the modal has been dismissed or cancelled. - `modal` — the rendered modal markup (`ReactElement | null`). You should always render this value into your component subtree. - `isDisplayed` — a boolean flag indicating if there is currently a pending modal for this definition. **The modal render function receives these properties**: ```tsx usePromiseModal(({ show, onDismiss, onSubmit }) => ( <MyModal show={show} onDismiss={onDismiss} onSubmit={onSubmit} /> )); ``` - `show` — boolean to tell if the window is visible or not. Used for in/out transitions. Primarily intended to be used as *react-bootstrap* Modal `show` property. - `onDismiss` — should be invoked when the modal is dismissed. Always resolves the promise to `undefined`. - `onSubmit` — should be invoked when the modal is submitted/confirmed. Resolves to the value provided as an argument to it. The resolve value cannot be `undefined`, because it is already reserved for dismissal. The function returns a *Promise* that is resolved with the submitted value, or `undefined` if it has been dismissed. ## Examples ### Confirmation You can easily implement a confirmation modal using `usePromiseModal()`: ```jsx import { usePromiseModal } from '@prezly/react-promise-modal'; function MyApp() { const confirmation = usePromiseModal(({ show, onSubmit, onDismiss }) => { // Use any modal implementation you want <MyConfirmationModal title="⚠️ Are you sure?" show={show} onConfirm={() => onSubmit(true)} onDismiss={onDismiss} /> }); async function handleDeleteAccount() { if (await confirmation.invoke()) { console.log('Conirmed'); } else { console.log('Cancelled'); } } return ( <div> <button onClick={handleDeleteAccount}>Delete account</button> {confirmation.modal} </div> ) } ``` ### Alert Alert is basically the same as confirmation, except there is no difference whether it is submitted or dismissed -- the modal has single action anyway. So we only need `onDismiss`: ```jsx import { usePromiseModal } from '@prezly/react-promise-modal'; function MyApp() { const alert = usePromiseModal(({ show, onDismiss }) => { // Use any modal implementation you want <MyAlertModal title="✔ Account deleted!" show={show} onDismiss={onDismiss} /> }); async function handleDeleteAccount() { await api.deleteAccount(); await alert.invoke(); } return ( <div> <button onClick={handleDeleteAccount}>Delete account</button> {alert.modal} </div> ) } ``` ### Prompt User Input For data prompts all you need is to resolve the promise by submitting the value to `onSubmit`: either a scalar, or more complex shapes wrapped into an object: ```tsx import { usePromiseModal } from '@prezly/react-promise-modal'; function MyApp() { const prompt = usePromiseModal<string, { title: string }>( (props) => <MyFilenamePromptModal {...props} />, ); async function handleCreateFile() { const filename = await prompt.invoke({ title: 'Please enter filename:' }); if (!filename) { console.error('Filename is required'); return; } await api.createFile(filename); } return ( <div> <button onClick={handleCreateFile}>Create new file</button> {prompt.modal} </div> ) } interface Props { title: string; show: boolean; onSubmit: (filename: string) => void; onDismiss: () => void; } function MyFilenamePromptModal({ title, show, onSubmit, onDismiss }: Props) { const [filename, setFilename] = useState("Untitled.txt"); return ( // Use any modal implementation you want <Modal> <form onSubmit={() => onSubmit(filename)}> <p>{title}</p> <input autoFocus value={filename} onChange={(event) => setFilename(event.target.value)} /> <button variant="secondary" onClick={onDismiss}>Cancel</button> <button variant="primary" type="submit">Confirm</button> </form> </Modal> ); } ``` ## Additional Invoke-time Arguments In addition to the three standard properties your modal render callback will always receive when rendered, you can pass extra call-time properties. Declare them with the second generic type parameter of `usePromiseModal()`, and then pass to the `invoke()` method: ```tsx import { usePromiseModal } from "@prezly/react-promise-modal"; const failureFeedback = usePromiseModal<undefined, { status: Status, failures: OperationFailure[] }>( ({ status, failures, show, onSubmit, onDismiss }) => ( <FailureModal status={status} failures={failures} show={show} onSubmit={onSubmit} onDismiss={onDismiss} /> ), ); // Invocation of the modal now requrires these additional properties: async function handleFlakyOperation() { const { status, failures } = await api.flakyOperation(); if (status !== 'success') { await failureFeedback.invoke({ status, failures }); // Note: here we pass additional parameters call-time } } ``` ------------------ # Credits Brought to you with :metal: by [Prezly](https://www.prezly.com/?utm_source=github&utm_campaign=react-promise-modal).