In React the declarative nature rules how we write our code today. With this rules in mind, showing modals, prompts or confirmation dialogs come with some additional cost of rethinking how state, effects must work.
Confirmation dialog in React the old way
Let's say we want to delete an item from a list, but before, that, we want to ask the user if he really wants to do that.
- The user clicks a delete button
- the click handler sets a state, which indicates that a deletion action was issued and that a confirmation is requested
- the component needs to render a confirmation dialog
- the user confirms the delete action by clicking on DELETE in the confirmation dialog
- the dialog sets the state to "confirmed"
- An effect is listening to the state and if we are in a deleting state and a confirmed state, we can actually execute some remote logic.
We could model the state as following
type State =
| null
| DELETE_REQUEST
| DELETE_CONFIRMED
| DELETE_PENDING
| DELETED
our code might then look like
const [state, setState] = useState<State>(null);
const handleDelete = () => {
if (state !== null) return;
setState('DELETE_REQUEST');
};
const handleConfirm = () => {
setState('DELETE_CONFIRMED');
};
useEffect(() => {
if (state === 'DELETE_CONFIRMED') {
setState('DELETE_PENDING');
deleteRemote().then(() => {
// setting to null resets to initial state.
setState(null);
});
}
}, [state]);
return (
<>
{state === 'DELETE_REQUEST' && (
<ConfirmDialog onConfirm={handleConfirm} />
)}
{state === 'DELETE_PENDING' && <Loading />}
{state === null && <Button onClick={handleDelete}>Delete now</Button>}
</>
);
Now this works usually perfectly fine. We have distinct state and we can easily and safely transition from one to another.
Still this is quite some boilerplate and when multiple asynchronous steps are involved, I usually do not want to put every single asynchronous step in a declarative way into states and react on them in a useEffect.
I believe an asynchronous prompt is possible in react and will solve many issues.
Prompting dynamic content
A lot of React programmers think of JSX as a magical data structure. It is not. It is just an object holding the component function and the props. It is basically an instruction of how to render things.
We can pass this jsx around the same way as anything else.
One thing I do dislike about prompts in react is, that if you have let's say 10 prompts and you would like to show them all after each other, than you would need to do a lot of state management and complex rendering.
because of this, I play around with this approach
// 👍 Consider this API
const [child, setChild] = useState();
setChild(<strong>Why is he putting JSX into state?</strong>)
// instead of this api
const [show, setShow] = useState(false);
setShow(true)
return (show && <strong>as usual</strong>)
What does this do? Nothing special, just putting the instruction how to render stuff, into state. returning child will work as usual.
This kind of unorthodox approach has one benefit. You could define what to render in a callback.
So if we slightly change this, we can do something like this:
const [promptContent, confirm] = useState();
const onClick = () => {
confirm(<strong>Are you sure?</strong>)
}
return promptContent
Now, when onClick is triggered, promptContent will contain the element. This is very powerful...
Asynchronous prompts
Now you think, well I can maybe render from within a callback, but this does not help a lot.
What if I tell you, that you can
- be in a callback
- render a prompt inside of the callback
- waiting for the user confirmation
- hide the prompt while still in the callback
- continue the callback and do remote operations.
So I say, we can do all this, in one single callback.
The key to it is that we need a hook which will additionally manage a promise.
const useConfirm = () => {
const promiseRef = useRef();
const [child, setChild] = useState();
const confirm = (jsxElement) => {
setChild(child);
// we return a promise and store
// the resolve
return new Promise((resolve, reject) => {
promiseRef.current = {resolve, reject}
});
}
const handleConfirm = () => {
// we hide the prompt by setting the jsx to null
setChild(null)
// we resolve the promise
promiseRef.current.resolve()
}
}
Now you could do
const onClick = async () => {
if (await confirm(...)) {
// ...
}
}
Wrapping up
I understand that this draft and idea are in a very unpolished state. But if you want to know more, leave me a comment.
Currently I am working on a library that will allow this kind of API and you will be able to bind it to the most popular UI libraries out there.
You can follow me for more updates at twitter.com/theluk246