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:
- You want to perform a mutation without navigating away from the current page
- You need to handle multiple concurrent mutations (like deleting multiple items)
- You want fine-grained control over the loading and error states
- 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 progresssubmitting
- Submission in progressloading
- 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>
);
}
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
- Type Safety : Use TypeScript to ensure type safety with your mutations:
const fetcher = useFetcher<typeof action>();
- Revalidation
: Use
useRevalidator
to refresh page data after successful mutations:
useEffect(() => {
if (fetcher.state === "idle" && fetcher.data) {
revalidator.revalidate();
}
}, [fetcher.state]);
- Error Handling : Always handle both success and error states:
if ("error" in fetcher.data) {
// Handle error case
}
- 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.