Adding Authentication to React AppsEntry 3 of 11

Sending Credentials in a Route Action

The auth route's action reads the mode from the request URL, sends the credentials to /login or /signup, and handles validation errors.

Notes

  • Actions receive the request, not hooks, so read query params with new URL(request.url).searchParams.
  • Default to 'login' if mode is missing, and reject unknown modes.
  • Backend responses to handle:
    • 422 (invalid input) or 401 (wrong credentials): return the error data so the form can show it with useActionData().
    • Other errors: throw, so the errorElement renders.
    • Success: store the token (Entry 4) and redirect('/').
  • Keep the HTTP call in a service module, matching Angular's service convention.

What / Why / When

What

Route-level login/signup handling.

Why

Submission, error feedback, and redirect use the same router patterns as other forms.

When

The auth form submits to a backend that returns a token.

Code compare

React
// services/auth.tsconst API = "http://localhost:8080";​export type AuthMode = "login" | "signup";export type Credentials = { email: string; password: string };export type AuthErrors = { message: string; errors?: Record<string, string> };export type AuthResult = { token: string } | AuthErrors;​export async function authenticate(mode: AuthMode, credentials: Credentials): Promise<AuthResult> {  const response = await fetch(`${API}/${mode}`, {    method: "POST",    headers: { "Content-Type": "application/json" },    body: JSON.stringify(credentials),  });​  if (response.status === 422 || response.status === 401) {    return response.json(); // { message, errors? }  }​  if (!response.ok) throw new Error("Could not authenticate user.");​  const { token } = await response.json();  return { token };}
React
// pages/Authentication.tsximport { data, redirect, type ActionFunctionArgs } from "react-router";import AuthForm from "../components/AuthForm";import { authenticate } from "../services/auth";import { storeAuthToken } from "../util/auth";​export default function AuthenticationPage() {  return <AuthForm />;}​export async function action({ request }: ActionFunctionArgs) {  const searchParams = new URL(request.url).searchParams;  const mode = searchParams.get("mode") || "login";​  if (mode !== "login" && mode !== "signup") {    throw data({ message: "Unsupported mode." }, { status: 422 });  }​  const formData = await request.formData();  const result = await authenticate(mode, {    email: String(formData.get("email")),    password: String(formData.get("password")),  });​  if (!("token" in result)) return result; // validation / credential errors → useActionData​  storeAuthToken(result.token);  return redirect("/");}
React
// components/AuthForm.tsx (additions)import { Form, Link, useActionData, useNavigation, useSearchParams } from "react-router";import type { AuthErrors } from "../services/auth";​export default function AuthForm() {  const [searchParams] = useSearchParams();  const actionData = useActionData() as AuthErrors | undefined;  const navigation = useNavigation();​  const isLogin = searchParams.get("mode") === "login";  const isSubmitting = navigation.state === "submitting";​  return (    <Form method="post">      <h1>{isLogin ? "Log in" : "Create a new user"}</h1>​      {actionData?.errors && (        <ul>          {Object.values(actionData.errors).map((err) => (            <li key={err}>{err}</li>          ))}        </ul>      )}      {actionData?.message && <p>{actionData.message}</p>}​      {/* ...email and password inputs */}​      <div>        <Link to={`?mode=${isLogin ? "signup" : "login"}`}>          {isLogin ? "Create new user" : "Login"}        </Link>        <button disabled={isSubmitting}>{isSubmitting ? "Submitting..." : "Save"}</button>      </div>    </Form>  );}
Angular
// services/auth.service.ts (excerpt; full version in Entry 4)authenticate(mode: 'login' | 'signup', credentials: { email: string; password: string }) {  return this.http    .post<{ token: string }>(`${this.api}/${mode}`, credentials)    .pipe(tap(({ token }) => this.storeToken(token)));}
Angular
// auth.component.ts (additions)export class AuthComponent {  private authService = inject(AuthService);  private router = inject(Router);​  protected isSubmitting = signal(false);  protected serverMessage = signal<string | null>(null);  protected serverErrors = signal<string[]>([]);​  onSubmit() {    if (this.form.invalid) return;    this.isSubmitting.set(true);​    this.authService      .authenticate(this.isLogin() ? "login" : "signup", this.form.getRawValue())      .subscribe({        next: () => this.router.navigate(["/"]),        error: (err: HttpErrorResponse) => {          if (err.status === 422 || err.status === 401) {            this.serverMessage.set(err.error.message);            this.serverErrors.set(Object.values(err.error.errors ?? {}));          }          this.isSubmitting.set(false);        },      });  }}

Differences and gotchas

  • React reads the mode from request.url inside the action; Angular reads it from the component's mode input.
  • Return (don't throw) expected auth errors in React. Throwing sends the user to the error page instead of showing the message on the form.

Quick reference

TaskReactAngular
Read mode in handlernew URL(request.url).searchParams.get('mode')this.mode()
Send credentialsauthenticate() service in actionAuthService.authenticate()
Show errorsreturn result → useActionData()error callback → signals
After successredirect('/')router.navigate(['/'])