Skip to main content

loader

A server-side function that loads data for a route before it renders.

Signature

export function loader(args: LoaderFunctionArgs): Promise<Response | Data> | Response | Data
args
LoaderFunctionArgs
required
Arguments passed to the loader function
return
Response | Data
Can return:
  • A Response object (with json(), redirect(), etc.)
  • Plain data (automatically serialized)
  • A Promise resolving to either

Basic Example

// app/routes/team.tsx
import { useLoaderData } from "react-router";

export async function loader({ params }: Route.LoaderArgs) {
  const team = await fetchTeam(params.teamId);
  return { team };
}

export default function Team() {
  const { team } = useLoaderData<typeof loader>();
  return <h1>{team.name}</h1>;
}

Returning Responses

import { redirect } from "react-router";

export async function loader({ request }: Route.LoaderArgs) {
  const user = await getUser(request);
  
  if (!user) {
    throw redirect("/login");
  }

  return Response.json({ user }, {
    headers: {
      "Cache-Control": "max-age=300"
    }
  });
}

Reading Request Data

export async function loader({ request, params }: Route.LoaderArgs) {
  const url = new URL(request.url);
  const searchQuery = url.searchParams.get("q");
  
  const cookie = request.headers.get("Cookie");
  const session = await getSession(cookie);

  const results = await searchProducts({
    query: searchQuery,
    userId: session.userId,
    category: params.category
  });

  return { results, query: searchQuery };
}

Using Context

// Server adapter setup
export default {
  async fetch(request, env) {
    return handleRequest(request, {
      getLoadContext: () => ({ env })
    });
  }
};

// Route module
export async function loader({ context }: Route.LoaderArgs) {
  // Access Cloudflare env, Express req/res, etc.
  const data = await context.env.DB.query(...);
  return { data };
}

Best Practices

Use the route-specific type for automatic param type inference:
// ✅ Types are inferred from your route config
export async function loader({ params }: Route.LoaderArgs) {
  params.teamId; // string (autocompleted)
}

// ❌ Generic types require manual annotation
export async function loader({ params }: LoaderFunctionArgs) {
  params.teamId; // unknown
}
Throw responses or errors to trigger error boundaries:
export async function loader({ params }: Route.LoaderArgs) {
  const product = await db.product.findUnique({
    where: { id: params.productId }
  });

  if (!product) {
    throw new Response("Not Found", { status: 404 });
  }

  return { product };
}
Only return JSON-serializable data:
// ❌ Dates, functions, and class instances don't serialize
return { createdAt: new Date() };

// ✅ Convert to strings
return { createdAt: new Date().toISOString() };
Use Promise.all() to load data in parallel:
export async function loader({ params }: Route.LoaderArgs) {
  const [user, posts, comments] = await Promise.all([
    fetchUser(params.userId),
    fetchPosts(params.userId),
    fetchComments(params.userId)
  ]);

  return { user, posts, comments };
}

See Also

Build docs developers (and LLMs) love