I am implementing an authentication page with react
and react-router-dom
where my login page is the index route of my homepage, I am making use of react-router-dom
Form
component that handle submission by sending it to the route action, I am returning an error object from the action when the inputs field are not valid and using the useActionData
hook to read the error, when the error returns and displayed to the user, the adress bar also changes by appending "/?index" to the "localhost:5173".
This happens only when the address bar have only the "localhost:5173" when i navigate to another route like `localhost:5173/forgot-password" there is no change in the url when i click submit button and return error from the action.
This is the code for my route action
import { LoaderFunctionArgs, redirect } from "react-router-dom";
interface Errors {
email?: string;
password?: string;
}
export default async function loginAction({ request }: LoaderFunctionArgs) {
const formData = await request.formData();
const email = formData.get("email") as string;
const password = formData.get("password") as string;
// const rememberMe = formData.get("remember-me");
const errors: Errors = {};
if (email === "") {
errors.email = "Please enter your email";
} else if (!email?.includes("@")) {
errors.email = "Not a valid email";
}
if (password === "") {
errors.password = "Please enter your password";
} else if (password?.length < 8) {
errors.password = "Password must be > 7 characters";
}
if (Object.keys(errors).length) {
return errors;
}
return redirect("/dashboard");
}
This is my Login
components that is using the useActionData
hook
import { useEffect, useRef, useState } from "react";
import { Link, Form, useNavigation, useActionData } from "react-router-dom";
const Login = () => {
const navigation = useNavigation();
const busy = navigation.state === "submitting";
const errors = useActionData() as { email: string; password: string };
const emailRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const [submitCount, setSubmitCount] = useState(0);
useEffect(() => {
if ((errors?.email || errors?.password) && submitCount > 0) {
errors?.email ? emailRef.current?.focus() : passwordRef.current?.focus();
}
}, [errors, submitCount]);
useEffect(() => {
emailRef.current?.focus();
}, []);
const handleSubmitCountIncrease = () => {
setSubmitCount((prevCount: number) => prevCount + 1);
};
return (
<>
<div className="flex h-[calc(100vh-100.21px)] flex-col items-center justify-center">
<div className="w-full max-w-[430px] rounded-2xl bg-white p-8 shadow">
<h2 className="mb-1 text-2xl font-medium text-[#2A303C]">Welcome!</h2>
<p className="text-sm text-[#2A303C]">
Please enter your credential to sign in!
</p>
{/* noValidate is used here for design purpose */}
<Form
action="."
method="post"
noValidate
className="mt-6 flex flex-col gap-5"
>
<div className="flex flex-col gap-1">
<label
htmlFor="email"
className="text-sm font-medium text-[#2A303C]"
>
Email
</label>
<input
type="email"
name="email"
id="email"
autoComplete="off"
required
disabled={busy}
ref={emailRef}
placeholder="Enter your email"
className={`${errors?.email && "border-2 border-red-400 focus:ring-0"} form-input rounded-md border-neutral-300 shadow-sm placeholder:text-xs focus:border-[#E87407] focus:outline-none focus:ring-1 focus:ring-[#E87407] focus:invalid:border-red-400 focus:invalid:ring-red-400 disabled:cursor-not-allowed disabled:opacity-50`}
/>
<p className="h-1 text-xs text-red-500">
{errors?.email && errors.email}
</p>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="password"
className="text-sm font-medium text-[#2A303C]"
>
Password
</label>
<input
type="password"
name="password"
id="password"
placeholder="Enter your password"
minLength={8}
autoComplete="off"
required
disabled={busy}
ref={passwordRef}
className={`${errors?.password && "border-2 border-red-400 focus:ring-0"} form-input rounded-md border-neutral-300 shadow-sm placeholder:text-xs focus:border-[#E87407] focus:outline-none focus:ring-1 focus:ring-[#E87407] placeholder-shown:focus:border-red-400 placeholder-shown:focus:ring-red-400 focus:invalid:border-red-400 focus:invalid:ring-red-400 disabled:cursor-not-allowed disabled:opacity-50`}
/>
<p className="h-1 text-xs text-red-500">
{errors?.password && errors.password}
</p>
</div>
<div className="flex justify-between">
<div className="flex items-center justify-center gap-2">
<input
type="checkbox"
name="remember-me"
id="remember-me"
disabled={busy}
className="focus:ring-none form-checkbox cursor-pointer rounded-sm border-neutral-300 text-[#E87407] focus:ring-[#E87407] focus:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50"
/>
<label htmlFor="remember-me" className="text-sm text-[#2A303C]">
Remember me
</label>
</div>
<Link
to={`forgot-password`}
className="font-semibold text-[#E87407]"
>
Forgot password?
</Link>
</div>
<button
type="submit"
disabled={busy}
onClick={handleSubmitCountIncrease}
className="w-full rounded-md bg-[#E87407] p-2 text-[#F9F7F0] disabled:cursor-not-allowed disabled:opacity-50"
>
{busy ? "Signing In..." : "Sign In"}
</button>
</Form>
</div>
</div>
</>
);
};
export default Login;
I have tried returning an error response that has a status code but did not work
import { LoaderFunctionArgs } from "react-router-dom";
interface Errors {
email?: string;
password?: string;
}
export default async function loginAction({ request }: LoaderFunctionArgs) {
const formData = await request.formData();
const email = formData.get("email") as string;
const password = formData.get("password") as string;
// const rememberMe = formData.get("remember-me");
const errors: Errors = {};
if (email === "") {
errors.email = "Please enter your email";
} else if (!email.includes("@")) {
errors.email = "Not a valid email";
}
if (password === "") {
errors.password = "Please enter your password";
} else if (password.length < 8) {
errors.password = "Password must be > 7 characters";
}
// return errors and status code other than 200 OK if there are errors
if (Object.keys(errors).length) {
return {
status: 400, // Bad Request
data: errors
};
}
// return redirection to dashboard if there are no errors
return redirect("/dashboard");
}
I did not expect the "/?index" to be appended and it is not happening when I'm in other routes except for the hompage...I need assistance in debugging what the issue could be.
This is completely normal behavior that can happen when submitting forms in components rendered on index routes. See Index Query Param for details.
You may find a wild
?index
appear in the URL of your app when submitting forms.Because of nested routes, multiple routes in your route hierarchy can match the URL. Unlike navigations where all matching route loaders are called to build up the UI, when a form is submitted only one action is called.
Because index routes share the same URL as their parent, the
?index
param lets you disambiguate between the two.createBrowserRouter([ { path: "/projects", element: <ProjectsLayout />, action: ProjectsLayout.action, children: [ { index: true, element: <ProjectsIndex />, action: ProjectsPage.action, }, ], }, ]); <Form method="post" action="/projects" />; // ProjectsLayout.action <Form method="post" action="/projects?index" />; // ProjectsPage.action
...
When a
<Form>
is rendered in an index route without anaction
, the?index
param will automatically be appended so that the form posts to the index route.
Based on your description it sounds like you are rendering Login
as the component for the root index route, and when the form is submitted the ?index
query parameter is automatically appended.
Example:
<Route path="/" element<MaybeSomeLayout />}>
<Route
index // <-- "/"
action={loginAction}
element={<Login />}
/>
...
</Route>