Viewing docs for an older release. View latest
On this page


Watch the 📼 Remix Single: Loading data into components

Each route can define a "loader" function that provides data to the route when rendering.

import { json } from "@remix-run/node"; // or cloudflare/deno

export const loader = async () => {
  return json({ ok: true });

This function is only ever run on the server. On the initial server render it will provide data to the HTML document, On navigations in the browser, Remix will call the function via fetch from the browser.

This means you can talk directly to your database, use server-only API secrets, etc. Any code that isn't used to render the UI will be removed from the browser bundle.

Using the database ORM Prisma as an example:

import { useLoaderData } from "@remix-run/react";

import { prisma } from "../db";

export async function loader() {
  return json(await prisma.user.findMany());

export default function Users() {
  const data = useLoaderData<typeof loader>();
  return (
      { => (
        <li key={}>{}</li>

Because prisma is only used in the loader it will be removed from the browser bundle by the compiler, as illustrated by the highlighted lines.

Type Safety

You can get type safety over the network for your loader and component with LoaderArgs and useLoaderData<typeof loader>.

import type { LoaderArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

export async function loader(args: LoaderArgs) {
  return json({ name: "Ryan", date: new Date() });

export default function SomeRoute() {
  const data = useLoaderData<typeof loader>();
  • will know that it's a string
  • will also know that it's a string even though we passed a date object to json. When data is fetched for client transitions, the values are serialized over the network with JSON.stringify, and the types are aware of that


Route params are defined by route file names. If a segment begins with $ like $invoiceId, the value from the URL for that segment will be passed to your loader.

// if the user visits /invoices/123
export async function loader({ params }: LoaderArgs) {
  params.invoiceId; // "123"

Params are mostly useful for looking up records by ID:

// if the user visits /invoices/123
export async function loader({ params }: LoaderArgs) {
  const invoice = await fakeDb.getInvoice(params.invoiceId);
  if (!invoice) throw new Response("", { status: 404 });
  return json(invoice);


This is a Fetch Request instance. You can read the MDN docs to see all of its properties.

The most common use cases in loaders are reading headers (like cookies) and URL URLSearchParams from the request:

export async function loader({ request }: LoaderArgs) {
  // read a cookie
  const cookie = request.headers.get("Cookie");

  // parse the search params for `?q=`
  const url = new URL(request.url);
  const query = url.searchParams.get("q");


This is the context passed in to your server adapter's getLoadContext() function. It's a way to bridge the gap between the adapter's request/response API with your Remix app.

This API is an escape hatch, it’s uncommon to need it

Using the express adapter as an example:

const {
} = require("@remix-run/express");

    getLoadContext(req, res) {
      // this becomes the loader context
      return { expressUser: req.user };

And then your loader can access it.

export async function loader({ context }: LoaderArgs) {
  const { expressUser } = context;
  // ...

Returning Response Instances

You need to return a Fetch Response from your loader.

export async function loader() {
  const users = await db.users.findMany();
  const body = JSON.stringify(users);
  return new Response(body, {
    headers: {
      "Content-Type": "application/json",

Using the json helper simplifies this, so you don't have to construct them yourself, but these two examples are effectively the same!

import { json } from "@remix-run/node"; // or cloudflare/deno

export const loader = async () => {
  const users = await fakeDb.users.findMany();
  return json(users);

You can see how json just does a little of the work to make your loader a lot cleaner. You can also use the json helper to add headers or a status code to your response:

import { json } from "@remix-run/node"; // or cloudflare/deno

export const loader = async ({ params }: LoaderArgs) => {
  const user = await fakeDb.project.findOne({
    where: { id: },

  if (!user) {
    return json("Project not found", { status: 404 });

  return json(user);

See also:

Throwing Responses in Loaders

Along with returning responses, you can also throw Response objects from your loaders. This allows you to break through the call stack and do one of two things:

  • Redirect to another URL
  • Show an alternate UI with contextual data through the CatchBoundary

Here is a full example showing how you can create utility functions that throw responses to stop code execution in the loader and show an alternative UI.

import { json } from "@remix-run/node"; // or cloudflare/deno
import type { ThrownResponse } from "@remix-run/react";

export type InvoiceNotFoundResponse = ThrownResponse<

export function getInvoice(id, user) {
  const invoice = db.invoice.find({ where: { id } });
  if (invoice === null) {
    throw json("Not Found", { status: 404 });
  return invoice;
import { redirect } from "@remix-run/node"; // or cloudflare/deno

import { getSession } from "./session";

export async function requireUserSession(request) {
  const session = await getSession(
  if (!session) {
    // You can throw our helpers like `redirect` and `json` because they
    // return `Response` objects. A `redirect` response will redirect to
    // another URL, while other  responses will trigger the UI rendered
    // in the `CatchBoundary`.
    throw redirect("/login", 302);
  return session.get("user");
import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import type { ThrownResponse } from "@remix-run/react";
import { useCatch, useLoaderData } from "@remix-run/react";

import { requireUserSession } from "~/http";
import { getInvoice } from "~/db";
import type { InvoiceNotFoundResponse } from "~/db";

type InvoiceCatchData = {
  invoiceOwnerEmail: string;

type ThrownResponses =
  | InvoiceNotFoundResponse
  | ThrownResponse<401, InvoiceCatchData>;

export const loader = async ({
}: LoaderArgs) => {
  const user = await requireUserSession(request);
  const invoice = getInvoice(params.invoiceId);

  if (!invoice.userIds.includes( {
    throw json(
      { invoiceOwnerEmail: },
      { status: 401 }

  return json(invoice);

export default function InvoiceRoute() {
  const invoice = useLoaderData<Invoice>();
  return <InvoiceView invoice={invoice} />;

export function CatchBoundary() {
  // this returns { data, status, statusText }
  const caught = useCatch<ThrownResponses>();

  switch (caught.status) {
    case 401:
      return (
          <p>You don't have access to this invoice.</p>
            Contact {} to get
    case 404:
      return <div>Invoice not found!</div>;

  // You could also `throw new Error("Unknown status in catch boundary")`.
  // This will be caught by the closest `ErrorBoundary`.
  return (
      Something went wrong: {caught.status}{" "}
Docs and examples licensed under MIT