Toast Notifications after API Actions in Remix
🔄 Flow of Your Delete + Toast Feature
1. Defining the Type (ActionResponse)
type ActionResponse = {
success: boolean;
actionType: string;
error?: string;
};
What: This describes the shape of data you expect back from your server after an action.
Why: So TypeScript can help you avoid mistakes — e.g., if you try to use
fetcher.data.successbut your backend doesn’t returnsuccess, TS will warn you.How:
success: boolean→ backend tells us if action worked or failed.actionType: string→ helps differentiate between multiple actions (update, delete, add, etc.).error?: string→ optional error message if things fail.
Without this type,
fetcher.datawould be{}(unknown), and you’d get errors likeProperty 'success' does not exist.
2. Creating the Fetcher
const fetcher = useFetcher<ActionResponse>();
What:
useFetcheris Remix’s special hook for running server actions without navigation (i.e., page won’t refresh/move).Why not a normal
<Form>?<Form>would cause navigation after submit → you’d lose your current page state.fetcherlets you send the request in background while staying on the same page.
Why the generic
<ActionResponse>?This tells TS “fetcher’s
.datawill followActionResponsestructure”.So now
fetcher.data.successworks safely.
3. The Delete Handler
const handleDelete = (section: keyof EditableData, index: number) => {
// 1. Update UI immediately (optimistic update)
setEditableData((prev) => ({
...prev,
[section]: prev[section].filter((_, i) => i !== index),
}));
// 2. Prepare request for backend
const updatedFormData = new FormData();
updatedFormData.append("actionType", "updateAllDetails");
updatedFormData.append("jobSeekerId", jobSeekerDetails?.jobseeker_id);
updatedFormData.append("section", section);
const updatedData = { ... }; // new educations, certs, etc.
updatedFormData.append("data", JSON.stringify(updatedData));
// 3. Send request using fetcher (background)
fetcher.submit(updatedFormData, { method: "post" });
};
Step 1: Immediately remove item from state (
setEditableData). → This is called optimistic UI → user sees instant feedback.Step 2: Build
FormDatato send structured info (jobSeekerId, which section, updated data).Step 3: Call
fetcher.submit→ sends to the same route’sactionfunction.Why not
fetch? Because Remix fetcher integrates directly with your route’saction, handles CSRF/session, and gives you.state,.data.
4. Listening to the Result
useEffect(() => {
if (fetcher.state === "idle" && fetcher.data) {
if (fetcher.data.success) {
toast.success("Item removed successfully.");
} else {
toast.error(fetcher.data.error || "Failed to remove item.");
}
}
}, [fetcher.state, fetcher.data]);
What: Wait until fetcher is finished (
state === "idle").Why: While
fetcher.state === "submitting", request is in progress → don’t show toast yet.How: Once backend responds:
If
success: true→ showtoast.success.Else → show
toast.error.
5. The Backend Action
if (actionType === "updateAllDetails" || actionType === "addAllDetails") {
try {
await client.PUT(`/jobseekers/update_jobseeker_accomplishments/{jobseeker_id}`, {
params: { path: { jobseeker_id } },
body: updateAllDetailsBody,
});
return json({ success: true, actionType });
} catch (error) {
return json({ success: false, actionType, error: "Failed to update details" }, { status: 500 });
}
}
What: This is where Remix actually updates your DB via API.
Why returning
{ success, actionType }?Because frontend (your
useEffect) depends on that shape.TS
ActionResponseensures consistency between backend return and frontend usage.
How:
On success → return
{ success: true }.On failure → return
{ success: false, error }.
🔑 Why Fetcher + Return Shape Were Necessary
Fetcher → avoids full page reload, integrates with Remix’s action, gives us
.state+.data→ perfect for background updates like delete.Return
{ success: boolean }→ frontend needs a simple yes/no to show toast.TypeScript
ActionResponse→ guarantees backend/frontend agree on response structure → prevents runtime bugs.
âś… So the complete story:
- You delete → UI updates immediately → fetcher submits new state → backend updates DB → returns
{ success }→ useEffect checks → shows toast accordingly.

