> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/expo/expo/llms.txt
> Use this file to discover all available pages before exploring further.

# Server-Side Rendering

> Learn about server-side rendering (SSR), static site generation (SSG), and React Server Components with Expo Router

# Server-Side Rendering

Expo Router supports server-side rendering (SSR) and static site generation (SSG) for web applications, enabling better performance and SEO.

<Note>
  Server rendering features require `@expo/server` and a compatible server environment.
</Note>

## Rendering Modes

### Client-Side Rendering (CSR)

Default mode - all rendering happens in the browser:

```tsx app/index.tsx theme={null}
export default function Home() {
  return <Text>Client-rendered</Text>;
}
```

### Server-Side Rendering (SSR)

Render on the server for each request:

```tsx app/blog/[slug].tsx theme={null}
import { Text } from 'react-native';

export async function loader({ params }) {
  const post = await fetchPost(params.slug);
  return { post };
}

export default function BlogPost({ loaderData }) {
  return <Text>{loaderData.post.title}</Text>;
}
```

### Static Site Generation (SSG)

Pre-render at build time:

```tsx app/blog/[slug].tsx theme={null}
export async function generateStaticParams() {
  const posts = await fetchAllPosts();
  return posts.map(post => ({ slug: post.slug }));
}

export async function loader({ params }) {
  const post = await fetchPost(params.slug);
  return { post };
}

export default function BlogPost({ loaderData }) {
  return <Text>{loaderData.post.title}</Text>;
}
```

From `CLAUDE.md:20-24`:

```
├── loaders/               # Data loader support (SSG and SSR)
├── rsc/                   # React Server Components  
├── static/                # Static rendering and SSR support
```

## Data Loading

### Loader Function

Fetch data on the server:

```tsx app/profile/[id].tsx theme={null}
import { useLoaderData } from 'expo-router';
import { View, Text } from 'react-native';

type LoaderData = {
  user: {
    id: string;
    name: string;
    email: string;
  };
};

export async function loader({ params }) {
  const response = await fetch(`https://api.example.com/users/${params.id}`);
  const user = await response.json();
  
  return { user };
}

export default function Profile() {
  const { user } = useLoaderData<LoaderData>();
  
  return (
    <View>
      <Text>{user.name}</Text>
      <Text>{user.email}</Text>
    </View>
  );
}
```

From the source (`hooks.ts:356-372`):

```typescript theme={null}
/**
 * Returns the result of the `loader` function for the calling route.
 *
 * @example
 * export function loader() {
 *   return Promise.resolve({ foo: 'bar' });
 * }
 *
 * export default function Route() {
 *  const data = useLoaderData<typeof loader>(); // { foo: 'bar' }
 *  return <Text>Data: {JSON.stringify(data)}</Text>;
 * }
 */
export function useLoaderData<T extends LoaderFunction = any>(): 
  LoaderFunctionResult<T>;
```

### Type-Safe Loaders

```tsx app/posts/[id].tsx theme={null}
import { useLoaderData } from 'expo-router';

export async function loader({ params }: { params: { id: string } }) {
  const post = await fetchPost(params.id);
  const comments = await fetchComments(params.id);
  
  return {
    post,
    comments,
  };
}

export default function Post() {
  // Automatically typed from loader return value
  const { post, comments } = useLoaderData<typeof loader>();
  
  return (
    <View>
      <Text>{post.title}</Text>
      {comments.map(comment => (
        <Text key={comment.id}>{comment.text}</Text>
      ))}
    </View>
  );
}
```

## Static Generation

### generateStaticParams

Generate static paths at build time:

```tsx app/products/[id].tsx theme={null}
export async function generateStaticParams() {
  const products = await fetchAllProducts();
  
  return products.map(product => ({
    id: product.id,
  }));
}

export async function loader({ params }) {
  const product = await fetchProduct(params.id);
  return { product };
}

export default function Product() {
  const { product } = useLoaderData();
  return <ProductView product={product} />;
}
```

From `CLAUDE.md:20-24`:

```
/** Skip routes created by `generateStaticParams()` */
skipStaticParams?: boolean;
```

### Incremental Static Regeneration

Revalidate static pages:

```tsx app/blog/[slug].tsx theme={null}
export const revalidate = 3600; // Revalidate every hour

export async function generateStaticParams() {
  const posts = await fetchRecentPosts();
  return posts.map(post => ({ slug: post.slug }));
}

export async function loader({ params }) {
  const post = await fetchPost(params.slug);
  return { post };
}

export default function BlogPost() {
  const { post } = useLoaderData();
  return <BlogPostView post={post} />;
}
```

## React Server Components

Use React Server Components for server-only code:

```tsx app/dashboard.tsx theme={null}
'use server';

import { db } from '../lib/database';

export default async function Dashboard() {
  // Runs only on server - can access database directly
  const stats = await db.stats.findMany();
  
  return (
    <View>
      {stats.map(stat => (
        <StatCard key={stat.id} data={stat} />
      ))}
    </View>
  );
}
```

### Client Components

Mark client-only components:

```tsx components/InteractiveChart.tsx theme={null}
'use client';

import { useState } from 'react';
import { View, Pressable } from 'react-native';

export function InteractiveChart({ data }) {
  const [selected, setSelected] = useState(null);
  
  return (
    <View>
      {data.map(item => (
        <Pressable
          key={item.id}
          onPress={() => setSelected(item)}
        >
          <ChartBar data={item} />
        </Pressable>
      ))}
    </View>
  );
}
```

## Server Configuration

### Next.js Adapter

Deploy with Next.js:

```javascript next.config.js theme={null}
const { withExpo } = require('@expo/next-adapter');

module.exports = withExpo({
  // Next.js config
  reactStrictMode: true,
  transpilePackages: [
    'react-native',
    'expo',
    'expo-router',
  ],
});
```

### Custom Server

Create a custom server:

```typescript server.ts theme={null}
import { createRequestHandler } from '@expo/server/adapter/express';
import express from 'express';

const app = express();

app.use(
  createRequestHandler({
    build: require('./dist/server'),
  })
);

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});
```

## SEO Optimization

### Meta Tags

Set meta tags for SEO:

```tsx app/blog/[slug].tsx theme={null}
import { Head } from 'expo-router/head';
import { useLoaderData } from 'expo-router';

export async function loader({ params }) {
  const post = await fetchPost(params.slug);
  return { post };
}

export default function BlogPost() {
  const { post } = useLoaderData();
  
  return (
    <>
      <Head>
        <title>{post.title}</title>
        <meta name="description" content={post.excerpt} />
        <meta property="og:title" content={post.title} />
        <meta property="og:description" content={post.excerpt} />
        <meta property="og:image" content={post.image} />
        <meta name="twitter:card" content="summary_large_image" />
      </Head>
      
      <BlogPostView post={post} />
    </>
  );
}
```

### Sitemap Generation

Generate sitemap.xml:

```tsx app/sitemap.xml+api.ts theme={null}
import { ExpoRequest, ExpoResponse } from 'expo-router/server';

export async function GET(request: ExpoRequest) {
  const posts = await fetchAllPosts();
  
  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
      <url>
        <loc>https://example.com/</loc>
        <changefreq>daily</changefreq>
        <priority>1.0</priority>
      </url>
      ${posts.map(post => `
        <url>
          <loc>https://example.com/blog/${post.slug}</loc>
          <lastmod>${post.updatedAt}</lastmod>
          <changefreq>weekly</changefreq>
          <priority>0.8</priority>
        </url>
      `).join('')}
    </urlset>
  `;
  
  return new Response(sitemap, {
    headers: {
      'Content-Type': 'application/xml',
      'Cache-Control': 'public, max-age=3600',
    },
  });
}
```

## Performance Optimization

### Data Caching

```tsx theme={null}
import { cache } from 'react';

const fetchUser = cache(async (id: string) => {
  const response = await fetch(`https://api.example.com/users/${id}`);
  return response.json();
});

export async function loader({ params }) {
  // Cached across requests
  const user = await fetchUser(params.id);
  return { user };
}
```

### Streaming

Stream data to the client:

```tsx app/feed.tsx theme={null}
import { Suspense } from 'react';

async function Posts() {
  const posts = await fetchPosts();
  return (
    <View>
      {posts.map(post => <PostCard key={post.id} post={post} />)}
    </View>
  );
}

export default function Feed() {
  return (
    <Suspense fallback={<LoadingSkeleton />}>
      <Posts />
    </Suspense>
  );
}
```

## Environment Variables

Access server-side environment variables:

```tsx app/api/config+api.ts theme={null}
export async function loader() {
  // Server-only variables
  const apiKey = process.env.API_KEY;
  const dbUrl = process.env.DATABASE_URL;
  
  // Never expose secrets to client
  return {
    publicConfig: {
      apiUrl: process.env.NEXT_PUBLIC_API_URL,
    },
  };
}
```

## Error Handling

### Custom Error Pages

```tsx app/+error.tsx theme={null}
import { ErrorBoundaryProps } from 'expo-router';
import { View, Text } from 'react-native';

export default function ErrorBoundary({ error, retry }: ErrorBoundaryProps) {
  return (
    <View>
      <Text>Something went wrong!</Text>
      <Text>{error.message}</Text>
      <Button onPress={retry} title="Try Again" />
    </View>
  );
}
```

### Loader Error Handling

```tsx theme={null}
export async function loader({ params }) {
  try {
    const data = await fetchData(params.id);
    return { data };
  } catch (error) {
    // Return error state
    return {
      error: {
        message: 'Failed to load data',
        status: 500,
      },
    };
  }
}

export default function Page() {
  const { data, error } = useLoaderData();
  
  if (error) {
    return <ErrorView error={error} />;
  }
  
  return <DataView data={data} />;
}
```

## Testing

### Test Loaders

```typescript __tests__/loaders/user.test.ts theme={null}
import { loader } from '../../app/user/[id]';

describe('User loader', () => {
  it('fetches user data', async () => {
    const result = await loader({ params: { id: '123' } });
    
    expect(result.user).toBeDefined();
    expect(result.user.id).toBe('123');
  });
  
  it('handles errors', async () => {
    const result = await loader({ params: { id: 'invalid' } });
    
    expect(result.error).toBeDefined();
  });
});
```

## Best Practices

### Minimize Client Bundle

```tsx theme={null}
// Good: Server component with server-only imports
'use server';
import { db } from '../lib/database'; // Won't be in client bundle

export default async function Page() {
  const data = await db.query();
  return <View>{data}</View>;
}
```

### Prefetch Data

```tsx theme={null}
import { router } from 'expo-router';

function ProductCard({ product }) {
  return (
    <Pressable
      onMouseEnter={() => {
        // Prefetch on hover
        router.prefetch(`/products/${product.id}`);
      }}
      onPress={() => {
        router.push(`/products/${product.id}`);
      }}
    >
      <Text>{product.name}</Text>
    </Pressable>
  );
}
```

### Cache Responses

```tsx theme={null}
export const revalidate = 3600; // 1 hour

export async function loader() {
  const data = await fetchData();
  return { data };
}
```

## Next Steps

<CardGroup cols={2}>
  <Card title="API Routes" icon="server" href="/router/api-routes">
    Create server-side API endpoints
  </Card>

  <Card title="Typed Routes" icon="code" href="/router/typed-routes">
    Type-safe server routes
  </Card>

  <Card title="Deep Linking" icon="link" href="/router/deep-linking">
    Server-rendered deep links
  </Card>

  <Card title="Navigation" icon="compass" href="/router/navigation">
    Client-side navigation
  </Card>
</CardGroup>
