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) andfetcher.data(the action's return value) are tracked per fetcher, separate from the globaluseNavigation.- Also offers
fetcher.load(href)to call a loader andfetcher.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
// 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> );}// 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.
@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
useFetcherreuses a route'sactionfrom 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
| React | Angular |
|---|---|
useFetcher() | Service call from component |
<fetcher.Form action="/newsletter"> | <form (ngSubmit)> + service |
fetcher.state / fetcher.data | Local signals |
fetcher.load(href) | Service get call |