Expo Router: Server vs Client Components for Route Data Fetching
Expo Router lets you fetch data in server components during the build, but mixing them with client components changes trust boundaries and bundle behavior. This note helps you decide where to place data fetching and verify the result.
29 Jan 2026, 07:21 UTC

The Problem: Where Should a Route Fetch Its Data?
In an Expo Router application, every file under app/ becomes a route. You can fetch data inside a client component with useEffect or a data library, or you can export an async function from the route file so Expo Router runs it as a server component during the build. The wrong choice slows startup, puts secrets in the client bundle, or breaks navigation when the device is offline.
Takeaway: use server components for read-only data that is safe to serialize and does not depend on device state. Keep client components for user input, local state, and real-time connections. Decide per route, not per project.
Requirements
- Route data must load before the screen renders, or show a loading state.
- Sensitive API keys must never ship to the client device.
- Nested layouts should not fetch the same data twice.
- The project must use Expo SDK 50 or later if server components are used; verify your installed SDK before relying on this feature.
The Smallest Suitable Design: Split by Data Sensitivity
Expo Router treats an async default export in a route file as a server component. A client component is a default export that does not await data directly.
app/
_layout.tsx # Root providers, client boundary
(marketing)/
about.tsx # Server component: fetch public content
(app)/
_layout.tsx # Authentication guard, client component
dashboard.tsx # Client component: calls /api/me with device token
settings.tsx # Client component: form with local state
A server component runs on the build server or during prerendering. Its output is serialized and sent to the React Native client. A client component runs on the device and can access device APIs.
Trust and Data Boundaries
- Trusted zone: the build environment. Safe for database queries and API keys.
- Untrusted boundary: the React Native client. It receives only serialized props and markup.
- Serializable props only: functions, class instances, and database connections cannot cross the boundary.
Route parameters such as params.id come from the URL and should be treated as untrusted input even inside a server component.
Operational Checks
Run these from the project root. No special file permissions are required, but the Expo CLI must be installed.
- Confirm SDK version:
Expected: SDK 50 or higher for server component support. Risk: older SDKs may ignore server component syntax or fail at build time.npx expo version - Inspect route files:
Expected: each route file exports a default component. Async default exports indicate server components.find app -name "*.tsx" -o -name "*.ts" | sort - Check for duplicate fetches:
Open React DevTools in the dev client and reload
/dashboard. Confirm the parent layout does not fetch the same endpoint as the child route. - Build and inspect bundle:
Expected: the command completes without serialization errors. Risk: passing a non-serializable prop to a server component fails the build with an opaque error.npx expo export --platform web --output-dir dist
Failure Modes
- Serialization errors: a server component returns a prop containing a Date instance or class. The build fails or the client receives undefined.
- Stale build data: server components fetch at build time. Content that changes after the build is not reflected until the next build.
- Client hydration mismatch: a client component reads localStorage during render while the server component did not, causing a mismatch warning.
- Bundle inflation: multiple nested server components each fetch the same list, increasing server time and duplicate payload.
Conditions That Would Change the Design
- Real-time features such as WebSockets or live chat require client components.
- Offline-first apps cannot rely on build-time fetching; use a local database and client sync instead.
- Data depends on device-specific input such as biometrics or location before the route loads.
- You are migrating an existing React Navigation stack; file-system routing may conflict with imperative navigation.
Concrete Example: Public Marketing Route
This server component fetches public content at build time. No secrets are involved.
// app/(marketing)/about.tsx
export default async function AboutPage() {
const res = await fetch('https://api.example.com/public/about');
const content = await res.json();
return (
<View>
<Text>{content.title}</Text>
</View>
);
}
Verify by running npx expo start --dev-client, opening the route, and confirming the text appears without a client-side loading spinner. Then run npx expo export --platform web and confirm the build succeeds.
Limitations and How to Check the Result
Server components do not have access to window, localStorage, or device APIs. If a route needs those, convert it to a client component and fetch inside useEffect or a data library. To check whether a route is behaving as a server component, add console.log('rendering server component') at the top of the route module; the log should appear in the build or server output, not only in the device console.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.