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

Loading Data with loader and useLoaderData

A route's loader runs before the route renders and fetches its data. The component reads the result with useLoaderData().

Notes

  • React Router waits for the loader to finish, then renders the page with the data ready. No useEffect, no loading state in the component. This is Angular's resolver.
  • The loader can return any value, including a fetch Response; React Router unwraps it, so useLoaderData() gets the parsed data.
  • useLoaderData() works in the route's element and any component rendered inside it, but not in parent routes or sibling routes.
  • Convention from the course: define and export the loader from the page component file, and keep the actual HTTP call in a services/ module (matching Angular's service pattern).
  • Loaders run in the browser, so they can use any browser API, but not React hooks.

What / Why / When

What

Route-level data fetching.

Why

Data loads in parallel with code, the component stays simple, and fetching starts as soon as navigation starts.

When

A page needs data to render.

Code compare

React
// services/events.tsexport type EventItem = {  id: string;  title: string;  image: string;  date: string;  description: string;};export type EventInput = Omit<EventItem, "id">;​const API = "http://localhost:8080";​export async function getEvents(): Promise<EventItem[]> {  const response = await fetch(`${API}/events`);  if (!response.ok) throw new Error("Could not fetch events.");  const data = await response.json();  return data.events;}
React
// pages/Events.tsximport { useLoaderData } from "react-router";import EventsList from "../components/EventsList";import { getEvents } from "../services/events";​export default function EventsPage() {  const events = useLoaderData<typeof loader>();  return <EventsList events={events} />;}​export function loader() {  return getEvents();}
React
// App.tsximport EventsPage, { loader as eventsLoader } from './pages/Events';​{  path: 'events',  element: <EventsRootLayout />,  children: [    { index: true, element: <EventsPage />, loader: eventsLoader },    // ...  ],}
Angular
// services/events.service.ts@Injectable({ providedIn: "root" })export class EventsService {  private http = inject(HttpClient);  private api = "http://localhost:8080";​  getEvents() {    return this.http      .get<{ events: EventItem[] }>(`${this.api}/events`)      .pipe(map((res) => res.events));  }}
Angular
// events/events.resolver.tsexport const eventsResolver: ResolveFn<EventItem[]> = () => inject(EventsService).getEvents();
Angular
// app.routes.ts{  path: 'events',  component: EventsRootLayoutComponent,  children: [    { path: '', component: EventsComponent, resolve: { events: eventsResolver } },  ],}
Angular
// events.component.ts@Component({  selector: "app-events",  imports: [EventsListComponent],  template: `<app-events-list [events]="events()" />`,})export class EventsComponent {  events = input.required<EventItem[]>(); // bound from resolve: { events }}

Differences and gotchas

  • React passes loader data through a hook; Angular binds resolved data to an input with the same key (needs withComponentInputBinding()).
  • Loaders return Promises; resolvers return Observables, Promises, or plain values.
  • For pages that should render immediately while data loads, Angular's httpResource() in the component is often better than a resolver. React Router's equivalent is returning an un-awaited promise from the loader and rendering it with <Suspense> + <Await>.

Quick reference

ReactAngular
loader on the routeresolve: { key: resolverFn }
useLoaderData()input() bound from resolve data
HTTP in services/ moduleHTTP in @Injectable service
<Await> for deferred datahttpResource() in component