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
fetchResponse; React Router unwraps it, souseLoaderData()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
// 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;}// 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();}// App.tsximport EventsPage, { loader as eventsLoader } from './pages/Events';{ path: 'events', element: <EventsRootLayout />, children: [ { index: true, element: <EventsPage />, loader: eventsLoader }, // ... ],}// 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)); }}// events/events.resolver.tsexport const eventsResolver: ResolveFn<EventItem[]> = () => inject(EventsService).getEvents();// app.routes.ts{ path: 'events', component: EventsRootLayoutComponent, children: [ { path: '', component: EventsComponent, resolve: { events: eventsResolver } }, ],}// 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
| React | Angular |
|---|---|
loader on the route | resolve: { key: resolverFn } |
useLoaderData() | input() bound from resolve data |
HTTP in services/ module | HTTP in @Injectable service |
<Await> for deferred data | httpResource() in component |