new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Expo · Router · all subjects

advanced-patterns

175 notes in this subject, read out of this brain and free to use. This is page 2 of 3.

Data loaders Runtime API access

Data loaders have full access to the Runtime API from `expo-server`. This includes utilities like `setResponseHeaders` for setting response headers and `StatusError` for throwing HTTP errors.

Data loaders Runtime API example

Example of using Runtime API functions in a data loader: ```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' }; } ```

Data loaders 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.

Data loaders environment variables example

Example of using environment variables in a data loader: ```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> ); } ```

Static vs server rendering for data loaders

Data loaders behave differently based on web.output configuration: Static rendering (web.output: 'static'): - Loader execution: Build time - `request` parameter: `undefined` - Best for: Blogs, marketing pages, documentation - Data is determined at build-time and does not change until the next build Server rendering (web.output: 'server'): - Loader execution: Request time - `request` parameter: ImmutableRequest - Best for: Personalized content, authentication-dependent pages - Loaders execute on every request

createStaticLoader helper function

`createStaticLoader` from `expo-router/server` creates loaders with improved type safety for routes that only need route parameters. The callback only receives the route params and is safe to use with both static and server rendering.

createStaticLoader example

Example of using createStaticLoader: ```tsx import { Text, View } from 'react-native'; import { useLoaderData } from 'expo-router'; import { createStaticLoader } from 'expo-router/server'; export const loader = createStaticLoader(async 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> </View> ); } ```

createServerLoader helper function

`createServerLoader` from `expo-router/server` creates loaders with improved type safety for routes that need access to the incoming HTTP request. The callback receives an `ImmutableRequest` and the route params as arguments. It will throw an error if called during static site generation because there is no HTTP request at build time.

createServerLoader example

Example of using createServerLoader: ```tsx import { Text, View } from 'react-native'; import { useLoaderData } from 'expo-router'; import { createServerLoader } from 'expo-router/server'; export const loader = createServerLoader(async (request, params) => { const authToken = request.headers.get('Authorization'); if (!authToken) { return { user: null }; } 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> ); } ```

LoaderFunction type for direct typing

You can type loaders directly using the `LoaderFunction` type from `expo-router/server`. This gives you full control over the function signature, including both `request` and `params`. The generic parameter specifies the return type of the loader.

LoaderFunction type example

Example of using LoaderFunction type directly: ```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> ); } ```

Data loaders known limitations

Known limitations of data loaders: 1. Loaders must return JSON-serializable data. Returning streams or async iterable from a loader is not supported. This will be addressed in a future release. 2. 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.

Loader exports dropped from client bundle

`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.

Dynamic redirects middleware example

Example of using middleware to perform dynamic redirects based on conditions: export default function middleware(request) { if (request.headers.has('specific-header')) { return Response.redirect('https://expo.dev'); } }

Server middleware feature availability

Server middleware is available in SDK 54 and later and is currently in alpha status. It requires a deployed server for production use.

Server middleware runs for every request

Server middleware in Expo Router runs for every HTTP request to the server before requests reach route handlers. Unlike API routes that handle specific endpoints, middleware runs for all requests and should execute quickly to avoid slowing down the app.

Client-side navigation bypasses middleware

Client-side navigation such as using <Link /> component or router imperatively in web apps, or native app screen transitions, will not pass through server middleware.

Enable server middleware configuration

To use server middleware, configure app.json with web output set to 'server' and add the expo-router plugin with unstable_useServerMiddleware set to true: { "expo": { "web": { "output": "server" }, "plugins": [ [ "expo-router", { "unstable_useServerMiddleware": true } ] ] } }

Create +middleware.ts file

Create a file named +middleware.ts in the src/app directory to define your server middleware function. The middleware function must be the default export of the file.

Middleware function signature and behavior

The middleware function receives an immutable request object and can return either a Response object or nothing. If middleware returns a Response, that response is sent immediately. If it returns nothing, the request continues to the matching route. The request object is immutable to prevent side effects.

Request/response flow with middleware

When a request comes to the app, Expo Router processes it in this order: 1) The middleware function runs first with an immutable request, 2) If middleware returns a Response, that response is sent immediately, 3) If middleware returns nothing, the request continues to the matching route, 4) The route handler processes the request and returns its response.

Configure middleware matchers

You can configure when middleware executes using unstable_settings with a matcher object. The matcher has two optional properties: methods (array of HTTP methods like ['GET', 'POST', 'PUT', 'DELETE']) and patterns (array of URL patterns). When both are specified, both conditions must be met for middleware to run.

Middleware pattern types

Matchers support four pattern types: Exact paths match only the specified path, e.g., '/api' matches '/api' but not '/api/users'. Named parameters like [postId] capture any single segment, e.g., '/posts/[postId]' matches '/posts/123' or '/posts/my-post'. Catch-all parameters like [...slug] capture one or more segments, e.g., '/blog/[...slug]' matches '/blog/2024' or '/blog/2024/12/post'. Regular expressions for complex patterns, e.g., /^\/api\/v\d+\/users$/ matches '/api/v1/users' but not '/api/users'. Middleware runs if any pattern matches the request URL.

Middleware execution scope

Expo Router supports a single root-level middleware file named +middleware.ts that runs for all server requests. Middleware executes only for requests that match specified patterns and methods, before any route matching or rendering occurs.

When middleware executes

Middleware executes for actual HTTP requests to the server including: initial page loads when a user first visits the site, full page refreshes, direct URL navigation, API route calls from any client (native/web apps, external services), and server-side rendering requests. Middleware does not run for: client-side navigation using <Link /> or router, native app screen transitions, prefetched routes, or static asset requests like images and fonts.

Request immutability details

The Request object passed to middleware is immutable. You can read all request properties like url, method, headers, read header values using request.headers.get(), check for header existence with request.headers.has(), and access URL parameters and query strings. You cannot modify headers with set(), append(), or delete(), consume the request body with text(), json(), or formData(), or access the body property directly.

Authentication middleware example

Example of using middleware to perform authorization checks: import { jwtVerify } from 'jose'; export default function middleware(request) { const token = request.headers.get('authorization'); const decoded = jwtVerify(token, process.env.SECRET_KEY); if (!decoded.payload) { return new Response('Forbidden', { status: 403 }); } }

Logging middleware example

Example of using middleware to log requests for debugging or analytics: export default function middleware(request) { console.log(`${request.method} ${request.url}`); }

API-only middleware example

Example of using matchers to run middleware only for API routes: export const unstable_settings = { matcher: { patterns: ['/api'], }, }; export default function middleware(request) { // Log all API requests for debugging console.log(`API request: ${request.method} ${request.url}`); // Add CORS headers for API routes const response = new Response(); response.headers.set('Access-Control-Allow-Origin', '*'); return response; }

Method-specific authentication middleware example

Example of protecting write operations (POST, PUT, DELETE) while allowing public read access: export const unstable_settings = { matcher: { methods: ['POST', 'PUT', 'DELETE'], patterns: ['/api', '/admin/[...path]'], }, }; export default function middleware(request) { const token = request.headers.get('authorization'); if (!token || !isValidToken(token)) { return new Response('Unauthorized', { status: 401 }); } } function isValidToken(token: string): boolean { // Your token validation logic return token.startsWith('Bearer '); }

Selective logging middleware example

Example of monitoring specific endpoints without logging every request: export const unstable_settings = { matcher: { patterns: ['/api/users/[userId]', '/admin', /^\/webhook/], }, }; export default function middleware(request) { const userAgent = request.headers.get('user-agent'); const timestamp = new Date().toISOString(); console.log(`[${timestamp}] ${request.method} ${request.url} - ${userAgent}`); }

Typed middleware example

Example of using TypeScript typing for middleware: import { MiddlewareFunction } from 'expo-router/server'; const middleware: MiddlewareFunction = request => { if (request.headers.has('specific-header')) { return Response.redirect('https://expo.dev'); } }; export default middleware;

Middleware best practices

Keep middleware lightweight because it runs synchronously on every server request and directly impacts response times. Use matchers to optimize performance by avoiding unnecessary middleware execution on routes that don't need it, especially for high-traffic applications. Prefer exact paths and named parameters over regex as simple patterns are faster to evaluate and easier to maintain. Combine method and pattern filtering for precise control over when middleware executes. For native apps, use API routes for secure data fetching, and note that when native apps call API routes, those requests will pass through middleware first.

Middleware limitations

Middleware runs exclusively on the server and only for HTTP requests. It does not execute during client-side navigation with <Link /> or native app screen transitions. The request object passed to middleware is immutable to prevent side effects. You can only have one root-level +middleware.ts in your app. The same limitations that apply to API routes also apply to middleware.

Initial routes included in HTML

All initial routes defined with unstable_settings = { initialRouteName: '...' } will be included in the initial HTML file as they are required for the first render. If the server request is for a modal, the screen rendered under the modal will also be included to ensure the modal is rendered correctly.

Async routes feature overview

Expo Router can automatically split JavaScript bundles based on route files using React Suspense. This enables faster development by only bundling routes that are navigated to, and can reduce initial bundle size. Routes are wrapped in suspense boundaries and loaded asynchronously, so the first navigation to a route takes longer but subsequent visits are instant and cached.

Async routes alpha status

Async routes is currently in alpha status.

Hermes Engine and async routes

Apps using the Hermes Engine will not benefit as much from bundle splitting as the bytecode is already memory mapped ahead of time. However, async routes will improve over-the-air updates, React Server Components, and web support even with Hermes.

Production native platforms disable suspense

When bundling for production on native platforms, all suspense boundaries will be disabled and there will be no loading states.

Error handling in async routes

Loading errors in async routes are handled in the parent route via the ErrorBoundary export.

Static analysis limitation with async routes

Async routes cannot be statically analyzed during development, so all files will be treated as routes even if they don't export a default component. After the component is bundled and loaded, any invalid route will use a fallback warning screen.

Clear Metro cache for async routes

When starting or exporting a project with async routes enabled, use the --clear flag to clear the Metro cache. This ensures routes are loaded asynchronously. Run: npx expo start --clear or npx expo export --clear

Static rendering in production web apps

Static rendering is supported in production web apps by rendering all Suspense boundaries synchronously in Node.js, then linking async chunks together in the HTML based on selected routes. All layout routes leading up to the leaf route for a URL are included in the initial server response to ensure consistent first render.

Async routes caveats

Async routes have the following limitations: they do not support native production apps yet; in development, the runtime JavaScript is lazily bundled so there may be cases where the HTML doesn't match the available JavaScript; custom SuspenseFallback exports do not work with async routes.

Expo Video DASH Support Module purpose

The Expo Video DASH Support Module is a local bare-expo module for experimenting with native expo-video transport providers on Apple platforms. It registers a VideoAssetTransportProvider at module startup and uses it to translate a narrow subset of SegmentBase DASH sources into HLS so they can be played by expo-video on iOS.

DASH Support Module demonstrates transport provider registration

The module demonstrates how a separate Expo module can register a native expo-video transport provider, how a provider can intercept matching DASH sources before playback begins, and how a DASH manifest can be translated into HLS as part of the asset-loading pipeline.

DASH Support Module limitations

The module is meant for demonstration and testing only, not production use. It is not a general-purpose DASH implementation. It only supports selected DASH sources that match the transport's assumptions and is focused on SegmentBase-style manifests without aiming to support all DASH variants. The transport logic is intentionally narrow and should not be treated as production-ready.

DASH to HLS translation workflow in expo-video

The workflow operates as follows: ExpoVideoDashSupportModule.swift registers SegmentBaseDASHToHLSVideoAssetTransportProvider in OnCreate. The provider checks incoming sources and only handles matching .mpd URLs. For a matching source, it builds a VideoAssetLoadPlan. The load plan starts a small local translation layer that exposes generated HLS playlists to AVFoundation. expo-video then loads the translated HLS output instead of the original DASH manifest.

DASH Support Module file structure

The module consists of the following files: expo-module.config.json which declares the Apple module entry point, ios/ExpoVideoDashSupportModule.swift which registers and unregisters the provider, and ios/SegmentBaseDASHToHLSTransport.swift which contains the demo DASH-to-HLS transport implementation.

DASH Support Module dependencies and deployment

The module depends on expo-video's native transport-provider APIs. It is intended to live alongside the app as a local module, but the same pattern can be used in a published Expo module if needed.

EAS Observe Expo Router integration overview

EAS Observe ships an opt-in integration for Expo Router that collects per-route metrics tagged with the route pattern. This lets you compare navigation performance by route in the dashboard instead of looking only at app-wide aggregates.

Expo Router integration SDK requirement

The Expo Router integration for EAS Observe is available on SDK 56 and later. On earlier SDKs, expo-observe still tracks app-wide metrics, but per-route navigation events are not emitted.

Enable Expo Router integration in EAS Observe

Call Observe.configure() with the 'expo-router' integration flag at module scope, before any screen mounts. The integration must be enabled before mount and cannot be toggled at runtime. Calling configure() after the app has mounted, or toggling the flag mid-session, throws an error.

Observe.configure() syntax for Expo Router integration

Example configuration in src/app/_layout.tsx: import { Observe } from 'expo-observe'; Observe.configure({ integrations: { 'expo-router': true }, });

useObserve hook for per-route metrics

Use the useObserve() hook to get a markInteractive function that is automatically scoped to the current route. The emitted event is tagged with the screen's route pattern. If the integration is disabled or expo-router is not installed, useObserve() falls back to the global Observe.markInteractive.

useObserve hook example usage

Example in src/app/(tabs)/index.tsx: import { useObserve } from 'expo-observe'; import { useEffect } from 'react'; export default function Home() { const { markInteractive } = useObserve(); useEffect(() => { markInteractive(); }, [markInteractive]); return (/* your screen content */); }

Filter sensitive URL parameters in Expo Router integration

Available in SDK 57 and later. Pass sensitive parameter keys to filteredParams in Observe.configure() to remove them from routeParams. The integration removes filtered keys from routeParams, the event omits url and includes urlHidden: true instead. routeName is not affected because it is a pattern and never contains parameter values.

filteredParams configuration syntax

Example in src/app/_layout.tsx: import { Observe } from 'expo-observe'; Observe.configure({ integrations: { 'expo-router': { filteredParams: ['userId', 'token'], }, }, });

cold_ttr metric definition

cold_ttr measures time from when a navigation action is dispatched (for example, a link click) to when the destination screen first becomes focused. For the very first focus after app launch, the measurement is taken from when the JS bundle is loaded, and the event includes isAppLaunch: true. Emitted at most once per screen instance within a session.

cold_ttr event parameters

cold_ttr event includes: routeName (string, route pattern like /(tabs)/sessions/[sessionId]), url (string, resolved pathname for the navigation), urlHidden (boolean, present as true when url is omitted because a parameter was filtered), routeParams (object, resolved route params like {sessionId: 'abc'}), isAppLaunch (boolean, true when measured against process start, false for subsequent navigation).

Give your agent this brain