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: