---
modificationDate: April 02, 2026
title: Data loaders
description: Learn how to fetch data on the server using data loaders in Expo Router.
isAlpha: true
---

<AgentInstructions>

## Submitting Feedback

If this page contains errors, outdated information, or gaps that blocked you from completing a task, report it so the docs can be improved:

curl -X POST https://api.expo.dev/v2/feedback/docs-send -H 'Content-Type: application/json' -d '{"url":"/router/web/data-loaders/","feedback":"🤖 Agent feedback: <specific, actionable description>"}'

Only submit when you have something specific and actionable to report.

</AgentInstructions>

# Data loaders

Learn how to fetch data on the server using data loaders in Expo Router.

> For the complete documentation index, see [llms.txt](/llms.txt). Use this file to discover all available pages.

> Data loaders are in [alpha](/more/release-statuses#alpha) and are available in SDK 55 and later. They require either [static rendering](/router/web/static-rendering) or [server rendering](/router/web/server-rendering).

Data loaders enable server-side data fetching for your routes. By exporting a `loader` function from a route file, you can fetch data on the server and access it in your component using the [`useLoaderData`](/versions/latest/sdk/router#useloaderdata) hook. This lets you keep sensitive data and API keys on the server while providing your components with the data they need.

## Setup

Enable data loaders in your project's [app config](/versions/latest/config/app) by adding the `unstable_useServerDataLoaders` option to the `expo-router` plugin:

```json
{
  "expo": {
    ... 
    "plugins": [
      [
        "expo-router",
        {
          "unstable_useServerDataLoaders": true,
          "unstable_useServerRendering": true
        }
      ]
    ]
  }
}
```

Configure your web output mode. Data loaders work with both [static rendering](/router/web/static-rendering) (`web.output: 'static'`) and [server rendering](/router/web/server-rendering) (`web.output: 'server'`):

```json
{
  "expo": {
    ... 
    "web": {
      ... 
      "output": "server"
    }
  }
}
```

Start the development server:

```sh
npx expo start
```

## Basic example

Export a `loader` function from your route file and use the [`useLoaderData`](/versions/latest/sdk/router#useloaderdata) hook to access the data in your component:

```tsx
import { Text, View } from 'react-native';
import { useLoaderData } from 'expo-router';

export async function loader() {
  // Fetch data from an API, database, or any server-side source
  const response = await fetch('https://api.example.com/data');
  return response.json();
}

export default function Home() {
  const data = useLoaderData<typeof loader>();

  return (
    <View>
      <Text>Data: {JSON.stringify(data)}</Text>
    </View>
  );
}
```

The `loader` function executes on the server, and its return value is serialized and passed to your component. This means you can safely use server-side secrets, database connections, and other resources that should not be exposed to the client. When using TypeScript, passing `typeof loader` as the generic parameter to [`useLoaderData`](/versions/latest/sdk/router#useloaderdata) allows the hook to infer the return type from your loader function.

> The [`useLoaderData`](/versions/latest/sdk/router#useloaderdata) hook does not need to be called in the route component itself. It can be called in any child component within the route's component tree.

### Using Suspense

When a component calls the [`useLoaderData`](/versions/latest/sdk/router#useloaderdata) hook while data is still loading, React suspends that component. The loading state cascades up the component tree until it reaches the nearest [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary, which then renders its fallback.

This lets you control exactly where loading fallbacks appear by placing [`<Suspense>`](https://react.dev/reference/react/Suspense) boundaries in your component tree:

```tsx
import { Suspense } from 'react';
import { Text, View } from 'react-native';
import { useLoaderData } from 'expo-router';

export async function loader() {
  const response = await fetch('https://api.example.com/data');
  return response.json();
}

export default function Home() {
  return (
    <View>
      <Text>Welcome</Text>
      <Suspense fallback={<Text>Loading...</Text>}>
        <DataSection />
      </Suspense>
    </View>
  );
}

function DataSection() {
  const data = useLoaderData<typeof loader>();
  return <Text>{data.title}</Text>;
}
```

In the above example, [`useLoaderData`](/versions/latest/sdk/router#useloaderdata) is in a child component of `<Home>` and wrapped it with [`<Suspense>`](https://react.dev/reference/react/Suspense) to show a loading state.

### Error handling

When a loader throws an error, it propagates to the nearest [error boundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary). You can export an [`ErrorBoundary`](/versions/latest/sdk/router#errorboundary) component from the same route file to handle loader errors:

```tsx
import { Text, View } from 'react-native';
import { useLoaderData, type ErrorBoundaryProps } from 'expo-router';

export async function loader() {
  const response = await fetch('https://api.example.com/data');
  if (!response.ok) {
    throw new Error('Failed to fetch data');
  }
  return response.json();
}

export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) {
  return (
    <View>
      <Text>Error: {error.message}</Text>
      <Text onPress={retry}>Try again</Text>
    </View>
  );
}

export default function DataPage() {
  const data = useLoaderData<typeof loader>();
  return (
    <View>
      <Text>{data.title}</Text>
    </View>
  );
}
```

When no `ErrorBoundary` is exported, the error propagates to the nearest parent route's error boundary. You can also use custom error boundary components within your route to catch errors at specific points in the component tree.

## Dynamic routes

Loaders receive route parameters as the second argument:

```tsx
import { Text, View } from 'react-native';
import { useLoaderData } from 'expo-router';

export async function loader(request, params) {
  const response = await fetch(`https://api.example.com/posts/${params.postId}`);
  return response.json();
}

export default function Post() {
  const data = useLoaderData<typeof loader>();

  return (
    <View>
      <Text>{data.title}</Text>
      <Text>{data.content}</Text>
    </View>
  );
}
```

## Accessing the request

> The `request` parameter is `undefined` when using static rendering because there is no HTTP request at build-time.

When using [server rendering](/router/web/server-rendering), loaders receive the incoming HTTP request as the first argument. This allows you to access headers, cookies, and other request information:

```tsx
import { Text, View } from 'react-native';
import { useLoaderData } from 'expo-router';

export async function loader(request) {
  // Access authorization header
  const authToken = request?.headers.get('Authorization');

  if (!authToken) {
    return { user: null };
  }

  // Fetch user data using the token
  const response = await fetch('https://api.example.com/user', {
    headers: { Authorization: authToken },
  });

  return { user: await response.json() };
}

export default function Profile() {
  const { user } = useLoaderData<typeof loader>();

  if (!user) {
    return <Text>Please log in</Text>;
  }

  return (
    <View>
      <Text>Welcome, {user.name}</Text>
    </View>
  );
}
```

## Returning data

Loaders can return data as plain JSON, which is easily deserialized using [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse). This includes objects, arrays or any other primitive that can be serialized with [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify).

```tsx
export async function loader() {
  const response = await fetch('https://api.example.com/data');
  return response.json();
}
```

If your loader returns `undefined` or `null`, the value is normalized to `null`.

## Runtime API

Data loaders have full access to the [Runtime API](/router/web/api-routes#runtime-api) from [`expo-server`](/versions/latest/sdk/server). This includes utilities for setting response headers, throwing HTTP errors, and running background tasks:

```tsx
import { setResponseHeaders, StatusError } from 'expo-server';

export async function loader(request) {
  const authToken = request?.headers.get('Authorization');

  if (!authToken) {
    throw new StatusError(401, 'Unauthorized');
  }

  setResponseHeaders({ 'Cache-Control': 'private, max-age=60' });

  return { user: 'authenticated' };
}
```

See the [Runtime API documentation](/router/web/api-routes#runtime-api) for a full list of available functions.

## Environment variables

Loaders run on the server and have access to `process.env`. Environment variables used in loaders are never exposed to the client bundle. This is useful for accessing API keys and other secrets:

```tsx
import { Text, View } from 'react-native';
import { useLoaderData } from 'expo-router';

export async function loader() {
  const apiKey = process.env.API_SECRET_KEY;

  const response = await fetch('https://api.example.com/data', {
    headers: { 'X-API-Key': apiKey },
  });

  return response.json();
}

export default function ApiData() {
  const data = useLoaderData<typeof loader>();

  return (
    <View>
      <Text>{JSON.stringify(data)}</Text>
    </View>
  );
}
```

## Difference between static and server rendering

Data loaders behave differently depending on your [`web.output`](/versions/latest/config/app#output) configuration:

| Aspect | Static Rendering | Server Rendering |
| --- | --- | --- |
| Loader execution | Build time | Request time |
| `request` parameter | `undefined` | [`ImmutableRequest`](/versions/latest/sdk/server#immutablerequest) |
| Best for | Blogs, marketing pages, documentation | Personalized content, authentication-dependent pages |

### Static rendering

With static rendering, loaders execute during when exporting your app with `npx expo export`. The data is embedded in the generated HTML and JSON files. This means:

-   Data is determined at build-time and does not change until the next build
-   The `request` parameter is `undefined` because there is no HTTP request during the build
-   Ideal for content that does not change frequently

### Server rendering

With server rendering, loaders execute on every request. This means:

-   The `request` parameter contains an immutable version of the incoming HTTP request
-   Requires [`expo-server`](/versions/latest/sdk/server) for production deployment

## Typed loader functions

For improved type safety, you can import the `LoaderFunction` type from `expo-router`:

```tsx
import { Text, View } from 'react-native';
import { useLoaderData } from 'expo-router';
import { type LoaderFunction } from 'expo-router/server';

type PostData = {
  title: string;
  content: string;
};

export const loader: LoaderFunction<PostData> = async (request, params) => {
  const response = await fetch(`https://api.example.com/posts/${params.postId}`);
  return response.json();
};

export default function Post() {
  const data = useLoaderData<typeof loader>();

  return (
    <View>
      <Text>{data.title}</Text>
      <Text>{data.content}</Text>
    </View>
  );
}
```

## Known limitations

-   Loaders **must** return JSON-serializable data. Streaming responses are currently not supported. This will be addressed in a future release.
-   Loader data is cached on the client during navigation. There is currently no built-in way to invalidate this cache. This will be addressed in a future release.

## Common questions

Can I use data loaders without server rendering?

Yes. Data loaders work with both static rendering (`web.output: 'static'`) and server rendering (`web.output: 'server'`).

Are loaders included in the client bundle?

No, `loader` exports are dropped from the client bundle. However, if another module contains server-side logic and is imported by client-side code outside of the **src/app** directory, it may be included in your client-side bundle.
