Server-Side Rendering
Expo Router supports server-side rendering (SSR) and static site generation (SSG) for web applications, enabling better performance and SEO.
Server rendering features require @expo/server and a compatible server environment.
Rendering Modes
Client-Side Rendering (CSR)
Default mode - all rendering happens in the browser:
export default function Home () {
return < Text > Client-rendered </ Text > ;
}
Server-Side Rendering (SSR)
Render on the server for each request:
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:
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:
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):
/**
* 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
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:
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:
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:
'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:
components/InteractiveChart.tsx
'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:
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:
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
Set meta tags for SEO:
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:
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' ,
},
});
}
Data Caching
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:
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:
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
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
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
__tests__/loaders/user.test.ts
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
// 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
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
export const revalidate = 3600 ; // 1 hour
export async function loader () {
const data = await fetchData ();
return { data };
}
Next Steps
API Routes Create server-side API endpoints
Typed Routes Type-safe server routes
Deep Linking Server-rendered deep links
Navigation Client-side navigation