Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Loaders: data from the server

A loader is a function a route module exports under the name loader. The framework calls it on the server before rendering the page and passes whatever it returns to the component. No useEffect and no "loading" state: the page reaches the browser with the data already in it.

A data module

Until there is a database, posts live in an array. Create app/data.server.ts:

// app/data.server.ts
export type Post = { id: number; title: string; body: string };
 
const posts: Post[] = [
  { id: 1, title: "Hello, world", body: "The first blog post." },
  { id: 2, title: "React Router 7", body: "Building a blog with the framework." },
];
 
export async function getPosts() {
  return posts;
}
 
export async function getPost(id: number) {
  return posts.find((post) => post.id === id) ?? null;
}

The .server suffix tells the framework this file is server-only. If you accidentally import it into code that ends up in the browser, the build stops with an error. The functions are async so that the array can later be replaced by a database without touching the pages.

The list of posts

Rewrite the home page app/routes/home.tsx:

// app/routes/home.tsx
import { Link } from "react-router";
import type { Route } from "./+types/home";
import { getPosts } from "../data.server";
 
export async function loader() {
  const posts = await getPosts();
  return { posts };
}
 
export default function Home({ loaderData }: Route.ComponentProps) {
  return (
    <ul>
      {loaderData.posts.map((post) => (
        <li key={post.id}>
          <Link to={`/posts/${post.id}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  );
}

http://localhost:5173/ shows a list of two posts. The type of loaderData is inferred from what loader returned — the editor suggests post.title.

The post page

The loader receives an object with params and request (the HTTP request itself):

// app/routes/post.tsx
import type { Route } from "./+types/post";
import { getPost } from "../data.server";
 
export async function loader({ params }: Route.LoaderArgs) {
  const post = await getPost(Number(params.postId));
  if (!post) {
    throw new Response("Post not found", { status: 404 });
  }
  return { post };
}
 
export default function Post({ loaderData }: Route.ComponentProps) {
  const { post } = loaderData;
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

/posts/1 shows the first post. For /posts/99 the loader throws a response with status 404 — the error boundaries lesson shows how to present that page.

When the loader runs

SituationWhere it runs
First visit to the pageOn the server, before rendering HTML
Navigating with LinkOn the server, the browser gets only the data
After a form submissionOn the server, data refreshes by itself

Common mistake

Returning everything from a loader — for example, a whole user record including the password hash. Everything a loader returns is serialized and sent to the browser, where it is visible in the page source. Pick only the fields the component needs.

Thanks for reading!

Donate to the site — we work hard for you.