Skip to main content
Version: 3.x

Mutate Data Using Client Side Fetcher

When building Front-Commerce applications with Remix, you have multiple options for handling data mutations. While the <Form> component works well for traditional form submissions that navigate to a new page, useFetcher is better suited for mutations that should happen without navigation or when you need more control over the mutation lifecycle.

When to Use useFetcher​

You should consider using useFetcher when:

  1. You want to perform a mutation without navigating away from the current page
  2. You need to handle multiple concurrent mutations (like deleting multiple items)
  3. You want fine-grained control over the loading and error states
  4. You need to programmatically trigger mutations (not just from form submissions)

Basic Usage​

Here's a basic example of using useFetcher for a mutation:

import { useFetcher } from "@front-commerce/remix/react";

function DeleteButton({ itemId }) {
const fetcher = useFetcher<typeof action>();

const handleDelete = () => {
fetcher.submit(null, {
method: "DELETE",
action: `/api/items/${itemId}`,
});
};

return (
<button onClick={handleDelete} disabled={fetcher.state !== "idle"}>
{fetcher.state === "submitting" ? "Deleting..." : "Delete"}
</button>
);
}

Using fetcher.Form​

For form-based mutations, fetcher.Form provides a convenient way to handle submissions without navigation. It works similarly to Remix's <Form> component but doesn't trigger page transitions. This makes it ideal for inline edits, like toggling favorites or updating settings:

import { IconButton } from "theme/components/atoms/Button";
import { useFetcher } from "@front-commerce/remix/react";

type RemoveFromWishlistProps = {
wishlistId: string | number;
itemId: string | number;
};

export default function RemoveFromWishlist({
wishlistId,
itemId,
}: RemoveFromWishlistProps) {
const fetcher = useFetcher<typeof action>();

return (
<fetcher.Form
method="delete"
action={`/api/customer/wishlists/${wishlistId}/items/${itemId}`}
>
<IconButton
type="submit"
appearance="block"
icon="cross"
iconSize="small"
notification={false}
state={fetcher.state === "submitting" ? "pending" : undefined}
title={"Remove from wishlist"}
/>
</fetcher.Form>
);
}

Advanced Usage with Status Handling​

For more complex scenarios, you might want to handle different states and responses. Here's a more complete example inspired by the useCartItemRemoveForm hook:

import { useFetcher } from "@front-commerce/remix/react";
import { useEffect, useMemo } from "react";
import { useRevalidator } from "@remix-run/react";

function useItemRemove() {
const fetcher = useFetcher<typeof action>();
const revalidator = useRevalidator();

// Trigger the mutation
const removeItem = (itemId) => {
fetcher.submit(null, {
method: "DELETE",
action: `/api/items/${itemId}`,
});
};

// Revalidate data after successful mutation
useEffect(() => {
if (fetcher.state === "idle" && fetcher.data) {
revalidator.revalidate();
}
}, [fetcher.state]);

// Handle different response states
const status = useMemo(() => {
if (!fetcher?.data) {
return {
hasMessage: false,
message: "",
success: false,
};
}
if ("error" in fetcher.data) {
return {
hasMessage: true,
message: fetcher.data.error,
success: false,
};
}
return {
hasMessage: Boolean(fetcher.data.errorMessage),
message: fetcher.data.errorMessage,
success: fetcher.data.success,
};
}, [fetcher.data]);

return {
status,
submissionState: fetcher.state,
removeItem,
};
}

Fetcher States​

The fetcher.state can be one of three values:

  • idle - No submission in progress
  • submitting - Submission in progress
  • loading - Submission completed, but page data is being reloaded

Form vs. Fetcher Comparison​

Here's when to use each approach:

Use <Form>​

  • When the mutation should result in a navigation
  • For traditional form submissions with multiple fields
  • When you want the browser's native form behavior
  • When you don't need fine-grained control over the mutation state
import { Form } from "@front-commerce/remix/react";

function CheckoutForm() {
return (
<Form method="post" action="/checkout">
{/* form fields */}
</Form>
);
}
info

For a detailed guide on handling mutations with the <Form> component, please refer to our Mutate Data Using Forms guide.

Use useFetcher​

  • For mutations that shouldn't trigger navigation
  • When handling multiple concurrent mutations
  • When you need programmatic control over the submission
  • For optimistic UI updates
  • When you need detailed control over loading and error states
function CartItemRemove({ itemId }) {
const { removeItem, status, submissionState } = useItemRemove();
return (
<div>
<button
onClick={() => removeItem(itemId)}
disabled={submissionState !== "idle"}
>
{submissionState === "submitting" ? "Removing..." : "Remove"}
</button>
{status.hasMessage && (
<p className={status.success ? "success" : "error"}>{status.message}</p>
)}
</div>
);
}

Best Practices​

  1. Type Safety : Use TypeScript to ensure type safety with your mutations:
const fetcher = useFetcher<typeof action>();
  1. Revalidation : Use useRevalidator to refresh page data after successful mutations:
useEffect(() => {
if (fetcher.state === "idle" && fetcher.data) {
revalidator.revalidate();
}
}, [fetcher.state]);
  1. Error Handling : Always handle both success and error states:
if ("error" in fetcher.data) {
// Handle error case
}
  1. Loading States : Provide feedback during mutations:
<button disabled={fetcher.state !== "idle"}>
{fetcher.state === "submitting" ? "Processing..." : "Submit"}
</button>

By following these patterns, you can create robust and user-friendly mutation handling in your Front-Commerce applications.

To learn more:​