Skip to main content

Command Palette

Search for a command to run...

Toast Notifications after API Actions in Remix

Published
•3 min read•View as Markdown
R

🎓 B.Tech CSE | 👨‍💻 Learning DSA & C++ | 🚀 Building projects & writing what I learn | 📚 Currently solving LeetCode & exploring Git/GitHub


🔄 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.success but your backend doesn’t return success, 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.data would be {} (unknown), and you’d get errors like Property 'success' does not exist.


2. Creating the Fetcher

const fetcher = useFetcher<ActionResponse>();
  • What: useFetcher is 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.

    • fetcher lets you send the request in background while staying on the same page.

  • Why the generic <ActionResponse>?

    • This tells TS “fetcher’s .data will follow ActionResponse structure”.

    • So now fetcher.data.success works 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 FormData to send structured info (jobSeekerId, which section, updated data).

  • Step 3: Call fetcher.submit → sends to the same route’s action function.

  • Why not fetch? Because Remix fetcher integrates directly with your route’s action, 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 → show toast.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 ActionResponse ensures 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

  1. Fetcher → avoids full page reload, integrates with Remix’s action, gives us .state + .data → perfect for background updates like delete.

  2. Return { success: boolean } → frontend needs a simple yes/no to show toast.

  3. 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.

More from this blog

I

Internship Learnings

17 posts