No More Messy Confirmation Code Inside Components.
Use react query hooks optimistically to reduce the confirmation code.
Introduction
Hooks are the main utility of the react query where we write the queries which cache the data and also the mutations to make the operations on the cached data or send request to the backend.
But imagine before deleting any sensitive data you want to show the confirmation to the user. The lines you see in every application "Are you sure you want to <operation>?". You see this in muliple websites, right? but let see how can we optimize the code for it using the react query hooks.
The Problem
// Before: Component bloated with confirmation + mutation logic
const TaskRow = ({ task }) => {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const deleteItem = async () => {
try {
setLoading(true);
await api.delete(`/task/${task.id}`);
toast.success("Deleted!");
refetchItems();
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
setOpen(false);
}
};
return (
<>
<button onClick={() => setOpen(true)}>Delete</button>
{open && (
<ConfirmModal
loading={loading}
error={error}
onConfirm={deleteItem}
onCancel={() => setOpen(false)}
/>
)}
</>
);
};
This is the one example of deletion but imagine you are developing a admin panel where you need to get lot of comfirmations / dielogues which are used at multiple areas of code sometime in single component we need two confirmations. How do we manage that? right. I was too have big question mark in my mind while I'm developing the admin panel where lots of confirmations and same dialogues for the multiple request to the backend some wanted to add some data in the payload before sending it to the backend.
Solution
The hook which gives us the asynchronous component.
// useConfirm.ts
import { useState, useCallback } from "react";
import {createPortal} from "react-dom";
export function useConfirm() {
const [isOpen, setIsOpen] = useState(false);
const [resolver, setResolver] = useState<(value: boolean) => void>(() => {});
const ask = useCallback(() => {
setIsOpen(true);
return new Promise<boolean>((resolve) => {
setResolver(() => resolve);
});
}, []);
const onConfirm = () => {
setIsOpen(false);
resolver(true);
};
const onCancel = () => {
setIsOpen(false);
resolver(false);
};
const DialogueUi = isOpen ? createPortal(
<ConfirmModal
loading={loading}
error={error}
onConfirm={onConfirm}
onCancel={onCancle}
/>)
return {
ask,
DialogueUi
};
}
Now the main react query hook we have
Recommended by LinkedIn
export function useDeleteTask(options) {
const mutation = useMutation(deleteTaskQuery, options);
const { ask, DialogueUi } = useConfirm();
const mutateWithConfirm = async (variables) => {
// a confirmation modal will be shown here
const ok = await ask(variables);
// if you click on the cancel the request will go back from here only
if (!ok) return;
// here you can get the
mutation.mutate(variables);
};
return { ...mutation, mutateWithConfirm, DialogueUi};
}
And this useDeleteTask can be used to delete the data which you can see it in below code.
const { mutateWithConfirm, isLoading, DialogueUi } = useDeleteTask({
onSuccess: () => {
toast.success("Deleted!");
queryClient.invalidateQueries(["tasks"]);
},
});
const handleDelete = () => {
mutateWithConfirm({ id: task.id, title: task.title });
};
return (
<>
<button disabled={isLoading} onClick={handleDelete}>
Delete Task
</button>
{DialogueUi}
</>
);
For updating the payload
Some times we have the dialogues which are used to add the data into the payload before sending them to the backend the in this case how you can we get that data let see
// the modal will return additional data selected by the user
const onConfirm = (data) => {
setIsOpen(false);
// this resolver send the data back to the react query hook
resolver({confirm:true,data:{..data}});
};
Above data we took inside the react hook and send it to the backend
const mutateWithConfirm = async (variables) => {
// showing the modal to the user by taking the success and data from it
const {success, data = {}} = await ask();
// if you click on the cancel the request will go back from here only
if (!success)return;
// here you that data will be appended inside the payload
mutation.mutate({...variables,...data});
};
Conclusion
Using this architecture, the component no longer carries the burden of confirmation logic or modal states. The React Query mutation remains responsible only for data changes, while the confirmation UI lives cleanly within the few components where it’s actually required.