# Routing Define URL routes in your full-page app so users can bookmark, share, and navigate between views with the browser's back and forward buttons. > Full-page apps is in private preview and you must add your app to the preview before you can use this feature. to request access. As your app grows beyond a single view, it needs routing. Routing is a way to map URLs to components so different views render depending on where the user navigates. The `@stripe/ui-extension-sdk/navigation` module provides a router for this purpose. Its APIs are designed primarily for [full-page apps](https://docs.stripe.com/stripe-apps/patterns/full-page-apps.md), where each route controls the path segment after your app’s base URL (for example, `/customers/cus_123` in `https://dashboard.stripe.com//app//customers/cus_123`). Because routes map to real Dashboard URLs, users can bookmark views, share links, and navigate with the browser’s back and forward buttons. ## Before you begin - Install the latest version of the [Stripe Apps CLI plugin](https://docs.stripe.com/stripe-apps/reference/cli.md#upgrade-the-cli). - [Create an app](https://docs.stripe.com/stripe-apps/create-app.md) or use an existing one with a `stripe.dashboard.fullpage` [viewport](https://docs.stripe.com/stripe-apps/how-ui-extensions-work.md#views-and-viewports). - To [generate](https://docs.stripe.com/stripe-apps/create-app.md#create-app) a new app, run `stripe apps create --full-page`. - To add the viewport to your existing app, install SDK version `9.2.0-alpha.x` or later and run `stripe apps add view --experimental` and choose `stripe.dashboard.fullpage`. ## Example Before you read through the details about how routing works, see the following example of a full-page app that uses routing. Click the navigation links and see how the Dashboard URL updates in the address bar as you transition between views—each route corresponds to a shareable URL: The following sections explain how to set up routing like this in your own app, starting with the route config. ## Define a route config Routing uses a route config—a single object that maps [names](https://docs.stripe.com/stripe-apps/routing.md#route-names) to [URL patterns](https://docs.stripe.com/stripe-apps/routing.md#path-patterns) and their corresponding [render functions](https://docs.stripe.com/stripe-apps/routing.md#render-function). You declare it once, and the rest of the system (the router, the navigation hooks, TypeScript support) derives its behavior from that definition. Use `createRoutes()` to build this route config. > We recommend placing all routes and the [`RouteRegister` interface](https://docs.stripe.com/stripe-apps/routing.md#enable-type-safe-navigation) in a dedicated `src/routes.ts` file. ```jsx // src/routes.ts import {createRoutes, route} from '@stripe/ui-extension-sdk/navigation'; import {Home} from './Home'; import {Customers} from './Customers'; import {Customer} from './Customer'; export const routes = createRoutes({ home: route('/', () => ), customers: route('/customers', () => ), customer: route( // Route name '/customers/:id', // URL patttern ({id}) => // Render function ), }); ``` ### Enable type-safe navigation After defining your routes, declare the `RouteRegister` interface so TypeScript can validate route names and parameters across your entire app. ```tsx // src/routes.ts (continued) export const routes = createRoutes({ ... }); declare module '@stripe/ui-extension-sdk/navigation' { interface RouteRegister { routes: typeof routes; } } ``` With this augmentation of the `RouteRegister` interface in place, the following type-safety features are enabled: - Passing invalid or missing parameters when [navigating to a route](https://docs.stripe.com/stripe-apps/routing.md#navigate-between-routes) causes a TypeScript error. - Access to type-safe route parameters when [reading the current route](https://docs.stripe.com/stripe-apps/routing.md#read-the-current-route). - Using an unknown route name when reading or updating the current route causes a TypeScript error. The following subsections describe each building block of a route definition in more detail. ### Route names Each route is identified by a name (`'home'`, `'customer'`) rather than a raw URL string. All navigation throughout your app uses these names, never raw paths. This indirection is intentional—if you later change `/customers/:id` to `/clients/:id`, you update the path in one place and every `setRoute('customer', {id})` call continues to work without modification. ### Path patterns Each full-page app owns a URL namespace under the Dashboard. Your route paths control the segment that follows your app’s base URL: ``` https://dashboard.stripe.com//app//customers/cus_123 └────────────────┘ Your route: /customers/:id ``` The path string in each `route()` call determines which URLs it matches. Patterns support static segments, required parameters, and optional parameters: | Pattern type | Example | | --- | --- | | Static | `/customers`, `/settings` | | Required parameters | `/customers/:id`, `/customers/:customerId/invoices/:invoiceId` | | Optional parameters | `/customers/:status?`, `/search/:query?` | | Mixed | `/customers/:id/invoices/:invoiceId?` | Dynamic segments use colon syntax—`/customers/:id` matches `/customers/cus_123` and passes `{id: 'cus_123'}` to the render function. Appending a question mark makes a segment optional, meaning `/labels/:name?` matches both `/labels` and `/labels/Deutsche-Grammophon`. > In [preview mode](https://docs.stripe.com/stripe-apps/create-app.md#preview-app), we validate route patterns at runtime to detect invalid definitions early. Segments must use lowercase alphanumeric characters, hyphens, and colons (for parameters) only. A pattern that violates these rules produces an error before your app reaches users. ### Render function When a URL matches a pattern, the router extracts the dynamic segments and passes them as the first argument to the render function. The second argument is the `ExtensionContextValue`—the same [context object](https://docs.stripe.com/stripe-apps/reference/extensions-sdk-api.md#props) the `FullPage` view receives. This means each route has access to environment information, such as the current user or the object being viewed. Types are inferred directly from the pattern string, so you get autocompletion and compile-time checking without writing any type annotations yourself: - Required parameters (`:id`) are a `string` type. - Optional parameters (`:id?`) are a `string | undefined` type. ```jsx export const routes = createRoutes({ // params: {}, context: ExtensionContextValue customers: route('/customers', (params, context) => ), // params: {id: string}, context: ExtensionContextValue customer: route('/customers/:id', ({id}, context) => ), // params: {id: string, invoiceId: string | undefined} invoice: route('/customers/:id/invoices/:invoiceId?', ({id, invoiceId}, context) => ( )), }); ``` Now that you have a route config, the next step is to wire it up to your view so the router can start matching URLs. ## Wire up the AppRouter A route config on its own is nothing more than data—you need to pass it to `AppRouter` for the router to start matching URLs and rendering components. Render `AppRouter` in your `stripe.dashboard.fullpage` view, and the router matches the URLs and renders the components: ```jsx // src/views/FullPage.tsx import {AppRouter} from '@stripe/ui-extension-sdk/navigation'; import {ExtensionContextValue} from '@stripe/ui-extension-sdk/context'; import {routes} from '../routes'; export default function FullPage(context: ExtensionContextValue) { return ( ); } ``` You can wrap `AppRouter` with `FullPageView` if every route shares the same layout. However, because different pages often need different structures such as a list page, a detail page with two columns, a settings form, we recommend defining the appropriate layout inside each route’s render function instead. This gives every route full control over its view structure: ```jsx import {FullPageView, Box} from '@stripe/ui-extension-sdk/ui'; import {createRoutes, route} from '@stripe/ui-extension-sdk/navigation'; import {Home} from './Home'; import {Customers} from './Customers'; import {Customer} from './Customer'; export const routes = createRoutes({ home: route('/', () => ( )), customers: route('/customers', () => ( )), customer: route('/customers/:id', ({id}) => ( )), }); ``` ### Redirect on unmatched routes When the current URL doesn’t match any defined route (a stale bookmark, a typo, or a path you’ve since removed) `AppRouter` performs a replace navigation to the route specified by `redirectOnNotFound`. This removes the unmatched URL from browser history so the user doesn’t get into a back-button loop: ```jsx ``` ## Navigate between routes With routes defined and the router wired up, your app has structure, but users also need to move between those routes in response to their actions. Examples of these actions include clicking a breadcrumb, selecting a row in a table, or submitting a form. The `useRoute()` hook, available in any component rendered within a route, provides both the current route information and two navigation mechanisms: ```jsx import {useRoute} from '@stripe/ui-extension-sdk/navigation'; function CustomerActions() { const {route, setRoute, createAppRoute} = useRoute(); console.log(route.key); // 'customer' console.log(route.routeParams); // {id: 'cus_123'} // ... } ``` The two mechanisms serve different purposes: `createAppRoute` for declarative link-based navigation, and `setRoute` for imperative programmatic navigation. ### Declarative navigation with Link Use `createAppRoute` to build a route descriptor and pass it as the `href` prop to [Link](https://docs.stripe.com/stripe-apps/components/link.md). This is the preferred approach for most navigation because it renders a real anchor element, giving users standard browser behavior such as hover to preview the URL, right-click to open in a new tab, and accessible keyboard navigation. ```jsx import {Link} from '@stripe/ui-extension-sdk/ui'; import {useRoute} from '@stripe/ui-extension-sdk/navigation'; function CustomerBreadcrumb() { const {createAppRoute} = useRoute(); return All customers; } ``` `createAppRoute` accepts the same arguments as `setRoute`—a route name and, for routes with dynamic segments, a parameters object: ```jsx const {createAppRoute} = useRoute(); createAppRoute('home'); createAppRoute('customers'); createAppRoute('customer', {id: 'cus_123'}); ``` The route that `createAppRoute` returns isn’t limited to the `Link#href`. Any components that accept [route descriptor](https://docs.stripe.com/stripe-apps/route-descriptors.md) as a prop(such as `DetailPage#breadcrumbs`) work with it too. This lets you build navigable UI elements, without imperative callbacks: ```jsx import {DetailPage, PageModule} from '@stripe/ui-extension-sdk/ui/experimental'; import {useRoute} from '@stripe/ui-extension-sdk/navigation'; function Customer({id, name}: {id: string; name: string}) { const {createAppRoute} = useRoute(); return ( {/* customer details */} } /> ); } ``` ### Imperative navigation with setRoute Not every navigation originates from a link. Use `setRoute` when a user action triggers navigation where a link doesn’t make semantic sense. For example, this can be after a form submission, in an `onPress` callback, or when navigating in response to a data table row click: ```jsx import {useCallback} from 'react'; import {useRoute} from '@stripe/ui-extension-sdk/navigation'; import {Box, Link} from '@stripe/ui-extension-sdk/ui'; function CustomerList() { const {setRoute} = useRoute(); const handleClick = useCallback( (item: {id: string}) => setRoute('customer', {id: item.id}), [setRoute], ); return ( setRoute('home')}>Back to home handleClick({id: 'cus_123'})}> View customer ); } ``` Because of the type registration you set up earlier, TypeScript enforces that `setRoute('customer', {id: item.id})` includes the required `id` parameter, and that `setRoute('home')` doesn’t accept any. The types flow from your route definitions through to every navigation call—a misspelled route name or a missing parameter is caught at compile time, not at runtime. ## Read the current route Sometimes you need to know *where* you are rather than navigate somewhere else. The `route` object returned by `useRoute()` tells you which route is currently active. When you check `route.key`, TypeScript narrows `route.routeParams` to the exact configuration declared by that route’s pattern—so you get type-safe access to parameters without casts or assertions: ```jsx import {useRoute} from '@stripe/ui-extension-sdk/navigation'; function CurrentRouteInfo() { const {route} = useRoute(); if (route.key === 'customer') { console.log(route.routeParams.id); // string — defined on /customers/:id // @ts-expect-error — 'name' doesn't exist on this route console.log(route.routeParams.name); } } ``` ## Handle redirects As your app evolves, you’ll mostly likely rename or restructure routes. But after an app is published, users might have URLs that they bookmarked or shared that point to the old paths, and those URLs must continue to work. The `Redirect` component solves this. Place it in a route’s render function to transparently forward an old path to its current destination: ```jsx import {FullPageView} from '@stripe/ui-extension-sdk/ui'; import {createRoutes, route, Redirect} from '@stripe/ui-extension-sdk/navigation'; import {Home} from './Home'; import {Customers} from './Customers'; import {Customer} from './Customer'; export const routes = createRoutes({ // Current routes home: route('/', () => ( )), customers: route('/customers', () => ( )), customer: route('/customers/:id', ({id}) => ( )), // Redirects from paths used in v1 of the app legacyUsers: route('/users', () => ), legacyUser: route('/users/:id', ({id}) => ( )), }); ``` `Redirect` immediately navigates to the route specified by `key`, forwarding any `params`. The user never sees the intermediate route, which means they go directly to the destination. As a general rule, if a URL was supported in any published version of your app, keep a redirect route for the old path so that bookmarks, shared links, and external references remain functional after you make route changes. ## See also - [Full-page apps](https://docs.stripe.com/stripe-apps/patterns/full-page-apps.md) - [Stripe Apps UI component reference](https://docs.stripe.com/stripe-apps/components.md)