Skip to main content

Summary

Create responses that contain headers and status without forcing serialization into an actual Response object. The data() function allows you to attach metadata like HTTP headers and status codes to your loader/action data while keeping type safety and avoiding premature serialization.

Signature

function data<D>(
  data: D,
  init?: number | ResponseInit
): DataWithResponseInit<D>

Parameters

data
D
required
The data to be included in the response. Can be any serializable value including objects, arrays, primitives, etc.
init
number | ResponseInit
Either a status code number or a ResponseInit object containing:
  • status - HTTP status code (e.g., 200, 201, 404)
  • statusText - Status text message
  • headers - Response headers object or Headers instance
If a number is provided, it’s used as the status code.

Returns

result
DataWithResponseInit<D>
A DataWithResponseInit instance containing the data and response initialization options.

Examples

Basic usage

Return data without any special headers or status:
export async function loader({ params }: Route.LoaderArgs) {
  const user = await getUser(params.id);
  return { user }; // Simple return, no need for data()
}

With status code

import { data } from "react-router";

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const item = await createItem(formData);
  
  // Return 201 Created status
  return data(item, 201);
}

With custom headers

import { data } from "react-router";

export async function loader({ params }: Route.LoaderArgs) {
  const user = await getUser(params.id);
  
  return data(
    { user },
    {
      headers: {
        "Cache-Control": "max-age=300, s-maxage=3600",
        "X-Custom-Header": "value",
      },
    }
  );
}

With status and headers

import { data } from "react-router";

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const item = await createItem(formData);
  
  return data(
    { item },
    {
      status: 201,
      headers: {
        "X-Custom-Header": "value",
      },
    }
  );
}

Common Use Cases

Setting cache headers

Control how browsers and CDNs cache your responses:
import { data } from "react-router";

export async function loader() {
  const posts = await getPosts();
  
  return data(
    { posts },
    {
      headers: {
        "Cache-Control": "public, max-age=300, s-maxage=3600",
      },
    }
  );
}

Error responses with status codes

import { data } from "react-router";

export async function loader({ params }: Route.LoaderArgs) {
  const user = await getUser(params.id);
  
  if (!user) {
    throw data(
      { error: "User not found" },
      { status: 404, statusText: "Not Found" }
    );
  }
  
  return { user };
}

Setting cookies

import { data } from "react-router";

export async function action({ request }: Route.ActionArgs) {
  const session = await getSession(request);
  session.set("userId", "123");
  
  return data(
    { success: true },
    {
      headers: {
        "Set-Cookie": await commitSession(session),
      },
    }
  );
}

Content negotiation

import { data } from "react-router";

export async function loader({ request }: Route.LoaderArgs) {
  const items = await getItems();
  
  return data(
    { items },
    {
      headers: {
        "Content-Type": "application/json; charset=utf-8",
        "Vary": "Accept",
      },
    }
  );
}

Rate limiting headers

import { data } from "react-router";

export async function action({ request }: Route.ActionArgs) {
  const result = await processRequest(request);
  
  return data(
    result,
    {
      headers: {
        "X-RateLimit-Limit": "100",
        "X-RateLimit-Remaining": "99",
        "X-RateLimit-Reset": "1640000000",
      },
    }
  );
}

Type Safety

The data() function preserves TypeScript types:
interface User {
  id: string;
  name: string;
}

export async function loader() {
  const user: User = await getUser();
  
  // Type is preserved: DataWithResponseInit<{ user: User }>
  return data({ user }, 200);
}

Comparison with Response

You can also return a native Response object:
// Using data() - keeps type safety
import { data } from "react-router";
export async function loader() {
  return data({ message: "Hello" }, 200);
}

// Using Response - loses type safety
export async function loader() {
  return new Response(
    JSON.stringify({ message: "Hello" }),
    {
      status: 200,
      headers: { "Content-Type": "application/json" },
    }
  );
}
Use data() when you want type safety and automatic serialization. Use Response when you need fine-grained control over the response format.
  • redirect - Create redirect responses
  • json - Deprecated v6 equivalent

Notes

  • You don’t need to use data() if you’re just returning plain data without custom headers/status
  • The data is not serialized until it’s sent to the client, allowing for efficient server-side processing
  • Headers are merged with any headers set by middleware or the framework
  • Status codes should follow HTTP standards for proper client handling

Build docs developers (and LLMs) love