Unable to navigate after fetcher.load() #10733
-
ReproductionI ran into a tricky problem with Remix involving fetchers. The original thing I wanted my app to do was be able to "reset" the action data after the user submits a form involving server-side validation. For example, the user could be submitting a form that has an email input in the sidebar of a page, and you want to do a server-side check on whether it's an email that already exists in the DB already, and print "This email is already taken" in the form as an error to the user. The trickiness comes after the user corrects the error and re-submits the form (which I'm using
What is the solution to this problem? System InfoSystem:
OS: Linux 6.10 Ubuntu 24.04 LTS 24.04 LTS (Noble Numbat)
CPU: (16) x64 AMD Ryzen 9 5900HX with Radeon Graphics
Memory: 11.89 GB / 30.76 GB
Container: Yes
Shell: 5.2.21 - /bin/bash
Binaries:
Node: 22.15.0 - /usr/local/bin/node
Yarn: 1.22.22 - ~/.yarn/bin/yarn
npm: 9.6.3 - /bin/npm
bun: 1.2.15 - ~/.bun/bin/bun
Browsers:
Brave Browser: 139.1.81.131
Chrome: 138.0.7204.157
npmPackages:
@remix-run/dev: ^2.15.1 => 2.17.0
@remix-run/express: ^2.15.1 => 2.17.0
@remix-run/node: ^2.15.1 => 2.17.0
@remix-run/react: ^2.15.1 => 2.17.0
vite: ^7.0.0 => 7.1.1 Used Package Manageryarn Expected BehaviorI am able to navigate to Actual BehaviorAn error occurs, and navigation does not occur. The error message says |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 2 replies
-
You are kind of swimming against the current. The error you're seeing is because Fetchers aren’t meant to be “reset” externally. The right flow is to return clean data from the action on success (null or an empty object), and return something like { error: "Email already used" } on failure. Then, conditionally display errors in your UI. This way, when the user fixes the input and resubmits, fetcher.data.error will clear out, and your error message will disappear naturally. |
Beta Was this translation helpful? Give feedback.
-
Context
Why Fix (minimum viable) import { useEffect } from "react";
import { useFetcher, useNavigate } from "@remix-run/react";
export default function EmailCheck() {
const navigate = useNavigate();
const fetcher = useFetcher({ key: "email-check" });
// trigger the load somewhere (button, submit, etc.)
// fetcher.load("/validate-email?value=...")
useEffect(() => {
if (fetcher.state === "idle" && fetcher.data) {
if (fetcher.data.ok) {
navigate("/success", { replace: true });
} else {
// reset UI/errors as needed
// e.g., show fetcher.data.message
}
}
}, [fetcher.state, fetcher.data, navigate]);
return (
<fetcher.Form method="get" action="/validate-email">
{/* ... */}
</fetcher.Form>
);
} Alternative patterns
// routes/validate-email.tsx
import { redirect } from "@remix-run/node";
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const value = url.searchParams.get("value");
const ok = await check(value);
return ok ? redirect("/success") : new Response(JSON.stringify({ ok }), {
headers: { "Content-Type": "application/json; charset=utf-8" },
});
}
// give the fetcher a stable key and change it to reset
const fetcher = useFetcher({ key: `email-check:${resetId}` }); o vuelve a estado limpio con un
Checklist
Docs
If this helps, please Mark as answer so others can find it. |
Beta Was this translation helpful? Give feedback.
Context
fetcher.load()
to validate/reset form stateload
completes, callingnavigate(...)
doesn’t change route or is flakyWhy
fetcher.load()
is asynchronous and drives a transition. If you callnavigate
in the same tick (or whilefetcher.state !== 'idle'
), the navigation can be swallowed by the in-flight fetcher transition or immediately reverted by a revalidation.Fix (minimum viable)
Use an effect that waits for the fetcher to become idle and then navigate based on the response.