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 1 of 3.

Zoom transition with Link.Preview: known limitation

When using Link.Preview in combination with zoom transitions, the target screen must use modal presentation, for example, presentation: 'fullScreenModal'. This is a limitation of the underlying iOS zoom transition API. When navigating to a non-modal screen from Link.Preview, the zoom transition will not work as expected and will fall back to standard navigation transition.

Link.AppleZoomTarget component usage

Link.AppleZoomTarget is used on the destination screen to specify the alignment of the zoomed element. It only accepts a single child component. Using Link.AppleZoomTarget is the normal way to control alignment on the destination screen.

Zoom transition with headers: known limitation

Avoid using zoom transitions when navigating between screens that have a header (navigation bar). There are known issues with the native iOS zoom transition API that can lead to visual glitches or unexpected behavior when headers are involved.

Link.AppleZoom must be used within a Link

Link.AppleZoom must be used as a direct or indirect child of a Link component with the asChild prop. Using it outside this context will result in an error.

Link.AppleZoom alignmentRect prop

The Link.AppleZoom component accepts an optional alignmentRect prop to control the alignment rectangle. The alignmentRect prop takes an object with x, y, width, and height properties. This prop internally relies on the alignmentRectProvider API and is normally not necessary if you use Link.AppleZoomTarget.

Zoom transition requires Stack navigator

The zoom transition feature is only supported when using the router's built-in Stack navigator. If you attempt to use Link with zoom transition to a screen that is not part of a Stack navigator, the zoom transition will not work as expected.

Zoom transition: basic example

To activate zoom transition for a link, wrap the source element with Link.AppleZoom inside a Link component with asChild prop. The destination screen defines the Image component normally, optionally wrapped with Link.AppleZoomTarget.

usePreventZoomTransitionDismissal with modal screens: known limitation

The usePreventZoomTransitionDismissal hook cannot be used in screens that have modal presentation, for example, presentation: 'fullScreenModal'. When used in a modal screen, the hook will not have any effect and dismissal gestures will function as normal.

Zoom transition: what it does

Zoom transitions provide a fluid animation effect when navigating between screens by zooming from a source element to the destination screen. This leverages iOS 18+ native zoom transition API to create shared, interactive transitions that produce spatial awareness between routes. For example, a card thumbnail may transition to become a full-width banner on the next route.

Link.AppleZoom component usage

Link.AppleZoom is a component that wraps the source element you want to zoom from. It marks the source of the zoom transition and is useful for including additional elements alongside the zoomed content. Link.AppleZoom only accepts a single child component; if you need to wrap multiple children, use a View or another container component.

Zoom transition dismissal delay: known issue

You may experience a noticeable delay of approximately 1 second when navigating to or dismissing screens that use zoom transitions, especially when performing rapid open/close/open gestures. This latency is higher than native iOS apps using the same zoom transition API. This is an upstream issue in react-native-screens related to how it handles transitions on iOS.

Zoom transition single child requirement

Both Link.AppleZoom and Link.AppleZoomTarget only accept a single child component. If you attempt to pass multiple children, a warning will be logged and the component will not render properly. To include multiple elements, wrap them in a container like View.

Zoom transition: iOS 18+ only, alpha API

Zoom transition is an alpha API available on iOS only in Expo SDK 55 and later. The API is subject to breaking changes. It requires iOS 18 or later; on older versions or other platforms, the component renders normally without zoom animation.

usePreventZoomTransitionDismissal hook

The usePreventZoomTransitionDismissal hook allows you to control the interactive swipe-to-dismiss gesture on screens using zoom transitions. Call the hook without options to completely disable swipe-to-dismiss. Use the unstable_dismissalBoundsRect option to define a rectangle where dismissal gestures are allowed, with properties minX, minY, maxX, maxY. This hook internally relies on the interactiveDismissShouldBegin API.

Other navigator options in Expo Router layouts

Expo Router layouts support various navigators beyond Stack and Tabs: Drawer navigator for drawer navigation, modals for displaying pages with transparency where parent navigator remains visible, and any navigator compatible with React Navigation including top tabs and bottom sheets.

withAnchor prop for forcing initial route in internal navigation

The withAnchor prop on Link forces the initial route (defined by initialRouteName) to be loaded when navigating directly into another stack inside your app. By default, initialRouteName is only considered during deep linking. Usage: <Link href='/stack/second' withAnchor>Go to second</Link>

Deep linking default support

Expo Router supports deep linking by default. Any page in your app can be linked to with a URL from outside your app. On mobile, a scheme is defined in the app config file, and this becomes the prefix for deep links. For example, with scheme 'myapp', you can deep link to src/app/about.tsx as myapp://about.

initialRouteName for deep link navigation hierarchy

The initialRouteName configuration in a layout's unstable_settings ensures that a specific page loads before a deep linked page, enabling proper back navigation. Set it in a layout file like src/app/stack/_layout.tsx with: export const unstable_settings = { initialRouteName: 'index' }.

SuspenseFallback parent precedence

When multiple parent layouts define a SuspenseFallback, the nearest parent takes precedence.

SuspenseFallback receives route parameters

The SuspenseFallback component receives route and params props. You can use params to display context-specific loading states for dynamic routes.

SuspenseFallback with params example

import { ActivityIndicator, Text, View } from 'react-native'; import { Stack, type SuspenseFallbackProps } from 'expo-router'; export function SuspenseFallback({ params }: SuspenseFallbackProps) { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Loading profile {params.id}...</Text> <ActivityIndicator size="large" /> </View> ); } export default function AppLayout() { return <Stack />; }

SuspenseFallback for loading states

Export a SuspenseFallback component from a layout file to customize the loading UI shown while any child route is suspended. Expo Router wraps each route in a React Suspense boundary. Custom suspense fallbacks are available in SDK 56 and later.

SuspenseFallback basic example

import { ActivityIndicator, View } from 'react-native'; import { Stack } from 'expo-router'; export function SuspenseFallback() { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <ActivityIndicator size="large" /> </View> ); } export default function RootLayout() { return <Stack />; }

ErrorBoundary example implementation

import { View, Text } from 'react-native'; import { type ErrorBoundaryProps } from 'expo-router'; export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) { return ( <View style={{ flex: 1, backgroundColor: "red" }}> <Text>{error.message}</Text> <Text onPress={retry}>Try Again?</Text> </View> ); } export default function Page() { ... }

ErrorBoundary export for error handling

Export an ErrorBoundary component from a route file to intercept and format component-level errors using React Error Boundaries. The ErrorBoundary receives error and retry props. When ErrorBoundary is not present, the error will be thrown to the nearest parent's ErrorBoundary.

SuspenseFallback limitation with async routes

Async routes do not support custom Suspense fallbacks.

Custom entry point for Expo Router

You can create a custom entry point (such as index.js in the project root) to initialize global services like analytics and error reporting, set up polyfills, or ignore specific logs using LogBox before the app loads the root layout. Import side effects and services first, then import 'expo-router/entry' last to ensure all configurations are set up before the app renders. Update the main property in package.json to point to the new entry file.

Expo Router static rendering support

Basic static rendering (SSG) is supported in Expo Router for web. Server-side rendering currently requires custom infrastructure to set up.

Expo Router enables Async Routes for bundle splitting

Async Routes (bundle splitting) in Expo Router improve development speed, especially in larger projects. They make upgrades easier as errors are isolated to a single route, allowing incremental updates or refactoring page-by-page rather than all at once.

Color API platform namespaces

The Color object has two platform-specific namespaces: Color.android.* for Android colors including base colors, attributes, and Material Design 3 colors; and Color.ios.* for iOS system colors from UIKit.

Android base colors via Color.android

Android base colors are accessed through Color.android.* and map to @android:color/ resources. Available colors include Color.android.black, Color.android.white, Color.android.transparent, Color.android.background_dark, and Color.android.background_light. The full list is available in the Android R.color documentation.

Color API import and purpose

The Color API is imported from 'expo-router' and provides type-safe access to platform-specific colors on Android and iOS. It wraps React Native's PlatformColor with full TypeScript support, enabling autocomplete and compile-time type checking for system colors.

Android theme colors re-render example

Example showing useColorScheme() to enable theme change re-renders: ```tsx import { Color } from 'expo-router'; import { View, Text, useColorScheme } from 'react-native'; function MyComponent() { // Triggers re-render when system theme changes useColorScheme(); return ( <View style={{ backgroundColor: Color.android.dynamic.surface }}> <Text style={{ color: Color.android.dynamic.onSurface }}>Hello, World!</Text> </View> ); } ```

Cross-platform color selection with Platform.select

The Color API is platform-specific. Use Platform.select to select the appropriate color for each platform, specifying ios, android, and default values. This ensures the correct platform-specific color system is used on each platform.

iOS system colors via Color.ios

iOS system colors are accessed through Color.ios.* and map directly to UIKit's standard colors and UI element colors. Example colors include Color.ios.systemBackground and Color.ios.label. iOS colors automatically adapt to the system appearance (light/dark mode) and accessibility settings.

Responding to Android theme changes with useColorScheme

To ensure components re-render when Android Material colors change with light/dark mode, use the useColorScheme() hook from React Native. Without calling useColorScheme(), colors may not update when the user switches between light and dark mode. This is especially important when using React Compiler, which can memoize components and skip re-renders unless useColorScheme() is called.

Material Design 3 dynamic colors via Color.android.dynamic

Material Design 3 dynamic colors are accessed through Color.android.dynamic.* and adapt to the user's wallpaper using Android's Dynamic Color feature, available on Android 12+ (API 31+). Available colors include Color.android.dynamic.primary, Color.android.dynamic.onPrimary, Color.android.dynamic.surface, and Color.android.dynamic.onSurface.

Cross-platform color usage example

Example showing Platform.select with Color API: ```tsx import { Platform, View, Text } from 'react-native'; import { Color } from 'expo-router'; function MyComponent() { const backgroundColor = Platform.select({ ios: Color.ios.systemBackground, android: Color.android.dynamic.surface, default: '#000000', }); const textColor = Platform.select({ ios: Color.ios.label, android: Color.android.dynamic.onSurface, default: '#FFFFFF', }); return ( <View style={{ backgroundColor }}> <Text style={{ color: textColor }}>Hello, World!</Text> </View> ); } ```

Material Design 3 static colors via Color.android.material

Material Design 3 static colors are accessed through Color.android.material.* using standard Material 3 Light/Dark theme colors. Available colors include Color.android.material.primary, Color.android.material.onPrimary, Color.android.material.primaryContainer, Color.android.material.onPrimaryContainer, Color.android.material.surface, and Color.android.material.onSurface.

Android attribute colors via Color.android.attr

Android theme attribute colors are accessed through Color.android.attr.* and resolve colors from the current theme using ?attr/ syntax. Available attributes include Color.android.attr.colorPrimary, Color.android.attr.colorSecondary, Color.android.attr.colorAccent, and Color.android.attr.colorBackground.

Share screens across navigators using shared routes or re-exports

In Expo Router, you can reuse a set of routes across multiple navigators either by migrating to shared routes or by creating multiple files and re-exporting the same component from them. When using groups or shared routes, navigate to specific tabs by using the fully qualified route name (e.g., /(home)/settings instead of /settings).

Track navigation state changes with usePathname, useSegments, and useGlobalSearchParams

To replace the NavigationContainer's onStateChange prop, use the usePathname(), useSegments(), and useGlobalSearchParams() hooks in conjunction with useEffect to observe changes. If tracking screen changes, follow the Screen Tracking guide.

Migrate screen tracking from React Navigation approach

Update screen tracking setup from the React Navigation screen tracking guide to the Expo Router screen tracking guide.

Avoid onReady and onStateChange from React Navigation

When migrating from React Navigation to Expo Router, avoid using onReady and onStateChange callbacks for screen tracking. The root NavigationContainer is not directly exposed in Expo Router, and these methods can cause cascading issues. Instead, use the URL-based approach with usePathname.

Screen tracking example with usePathname and useGlobalSearchParams

import { useEffect } from 'react'; import { usePathname, useGlobalSearchParams, Slot } from 'expo-router'; export default function Layout() { const pathname = usePathname(); const params = useGlobalSearchParams(); // Track the location in your analytics provider here. useEffect(() => { analytics.track({ pathname, params }); }, [pathname, params]); // Export all the children routes in the most basic way. return <Slot />; } This example shows how to create a root layout that tracks route changes by observing pathname and query parameters, then notifying an analytics provider whenever either changes.

Screen tracking implementation with usePathname

To implement screen tracking for analytics in Expo Router, create a root layout component that uses usePathname() to observe the current URL and track it in an analytics provider. Unlike React Navigation, Expo Router always has access to a URL, making screen tracking as simple as tracking URLs on the web.

Data loaders return value

Loaders must return JSON-serializable data. Loaders can return objects, arrays, or any primitive that can be serialized with JSON.stringify(). If your loader returns `undefined` or `null`, the value is normalized to `null`.

Data loader request parameter

When using server rendering, loaders receive the incoming HTTP request as the first argument. This allows you to access headers, cookies, and other request information. The `request` parameter is `undefined` when using static rendering because there is no HTTP request at build-time.

Dynamic route loader example

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

Data loaders with dynamic routes

Loaders receive route parameters as the second argument. This allows you to access dynamic route segments like [postId] in your loader function.

Data loader error handling example

Example of handling loader errors with ErrorBoundary: ```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> ); } ```

Data loader error handling with ErrorBoundary

When a loader throws an error, it propagates to the nearest error boundary. Export an `ErrorBoundary` component from the same route file to handle loader errors. When no ErrorBoundary is exported, the error propagates to the nearest parent route's error boundary.

Data loader Suspense example

Example of using Suspense with data loaders: ```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>; } ```

Data loaders with Suspense

When a component calls `useLoaderData` while data is still loading, React suspends that component. The loading state cascades up until it reaches the nearest `<Suspense>` boundary, which renders its fallback. You can control where loading fallbacks appear by placing `<Suspense>` boundaries in your component tree.

useLoaderData hook placement

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

Basic data loader example

Export an async `loader` function from a route file and use `useLoaderData<typeof loader>()` to access the data in your component: ```tsx 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() { const data = useLoaderData<typeof loader>(); return ( <View> <Text>Data: {JSON.stringify(data)}</Text> </View> ); } ```

Enable data loaders in app config

Enable data loaders in app.json by adding `unstable_useServerDataLoaders: true` to the expo-router plugin configuration. This also requires setting `unstable_useServerRendering: true`.

Data loader accessing request headers example

Example of accessing request headers and cookies in a data loader: ```tsx import { Text, View } from 'react-native'; import { useLoaderData } from 'expo-router'; export async function loader(request) { 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> ); } ```

Data loaders availability and requirements

Data loaders are in alpha and are available in SDK 55 and later. They require either static rendering (web.output: 'static') or server rendering (web.output: 'server').

Data loaders overview

Data loaders enable server-side data fetching for 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` hook. This allows you to keep sensitive data and API keys on the server while providing components with the data they need.

Give your agent this brain