Building a Multi-Page SPA with React RouterEntry 18 of 20

Updates Without Navigation: useFetcher

useFetcher() calls a loader or action without navigating. It's an isolated mini-form for in-place updates.

Notes

  • <fetcher.Form method="post" action="/newsletter"> submits to that route's action, but the user stays on the current page.
  • fetcher.state (idle | submitting | loading) and fetcher.data (the action's return value) are tracked per fetcher, separate from the global useNavigation.
  • Also offers fetcher.load(href) to call a loader and fetcher.submit(data, options) to submit programmatically.
  • Ideal for: newsletter signups in a footer, likes, toggles, inline edits, any widget that appears on many pages.

What / Why / When

What

Background loader/action calls.

Why

Reuse route actions without leaving the page.

When

The form or update lives in shared UI (header, footer, sidebar) or should not change the URL.

Code compare

React
// components/NewsletterSignup.tsximport { useEffect } from "react";import { useFetcher } from "react-router";​type NewsletterResult = { message: string };​export default function NewsletterSignup() {  const fetcher = useFetcher<NewsletterResult>();  const { data, state } = fetcher;​  useEffect(() => {    if (state === "idle" && data?.message) {      window.alert(data.message);    }  }, [data, state]);​  return (    <fetcher.Form method="post" action="/newsletter">      <input        type="email"        name="email"        placeholder="Sign up for newsletter..."        aria-label="Sign up for newsletter"      />      <button disabled={state === "submitting"}>Sign up</button>    </fetcher.Form>  );}
React
// pages/Newsletter.tsxexport async function action({ request }: ActionFunctionArgs) {  const email = String((await request.formData()).get("email"));  await subscribeToNewsletter(email); // services/newsletter.ts  return { message: "Signup successful!" };}

A component calls a service directly; local signals hold the state.

Angular
@Component({  selector: "app-newsletter-signup",  imports: [FormsModule],  template: `    <form (ngSubmit)="signup()">      <input        type="email"        name="email"        [(ngModel)]="email"        placeholder="Sign up for newsletter..."      />      <button [disabled]="isSubmitting()">Sign up</button>    </form>  `,})export class NewsletterSignupComponent {  private newsletterService = inject(NewsletterService);  protected email = "";  protected isSubmitting = signal(false);​  signup() {    this.isSubmitting.set(true);    this.newsletterService.subscribe(this.email).subscribe({      next: () => alert("Signup successful!"),      complete: () => this.isSubmitting.set(false),      error: () => this.isSubmitting.set(false),    });  }}

Differences and gotchas

  • useFetcher reuses a route's action from anywhere; Angular just injects the service anywhere.
  • Fetcher submissions still revalidate loaders after an action (unlike plain service calls), keeping visible data fresh.

Quick reference

ReactAngular
useFetcher()Service call from component
<fetcher.Form action="/newsletter"><form (ngSubmit)> + service
fetcher.state / fetcher.dataLocal signals
fetcher.load(href)Service get call