RyzeDeskRyzeDesk

Stripe

4105 articles

Routing


Public preview

Routing Public preview

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.

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 @the relevant part of the product module provides a router for this purpose. Its APIs are designed primarily for full-page apps, where each route controls the path segment after your app’s base URL (for example, /customers/cus_123 in https://dashboard.stripe.com/<ACCOUNT_ID>/app/<the related setting>/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.

Routing has three pieces that work together:

  • A route config created with createRoutes() that maps route names to URL patterns and render functions.
  • A NavigationProvider that owns the route config and makes it available to the router and to navigation hooks anywhere in your app – including non-full-page viewports such as stripe. dashboard. drawer. default .
  • An AppRouter that matches the current URL against the config and renders the matching route inside your full-page view.

Before you begin

  • Install the latest version of the Stripe Apps CLI plugin .
  • Create an app or use an existing one with a stripe. dashboard. fullpage viewport .
  • To generate a new app, run stripe apps create <app-name> --full-page .
  • To add the viewport to your existing app, install SDK version 9. 2. 1 or later and run stripe apps add view 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:

Loading example...

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 to URL patterns and their corresponding render functions. You declare it once, and the rest of the system (the router, the navigation hooks, TypeScript support) derives its behaviour from that definition. Use createRoutes() to build this route config.

Note

We recommend placing all routes and the RouteRegister interface in a dedicated the relevant part of the product file.

Enable type-safe navigation

After defining your routes, declare the RouteRegister interface so TypeScript can validate route names and parameters across your entire app.

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 causes a TypeScript error.
  • Access to type-safe route parameters when reading 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 navigateToAppRoute('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/<ACCOUNT_ID>/app/<APP_ID>/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 typeExample
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.

Caution

In preview mode, 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 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.
export const routes = createRoutes({
 // params: {}, context: ExtensionContextValue
 customers: route('/customers', (params, context) => <Customers />),
 // params: {id: string}, context: ExtensionContextValue
 customer: route('/customers/:id', ({id}, context) => <Customer id={id} />),
 // params: {id: string, invoiceId: string | undefined}
 invoice: route('/customers/:id/invoices/:invoiceId?', ({id, invoiceId}, context) => (
 <Invoice customerId={id} invoiceId={invoiceId} />
 )),
});

Now that you have a route config, the next step is to make it available to your app with NavigationProvider.

Provide routes with NavigationProvider

A route config on its own is nothing more than data. NavigationProvider connects it to your app. Wrap the root of your view in NavigationProvider and pass your route config to its routes prop:

The navigation hooks work in any component beneath the provider, whether or not an AppRouter is rendered. This is what lets you navigate from non-full-page viewports such as stripe.dashboard.drawer.default.

Note

Render a single NavigationProvider near the root of each view rather than wrapping individual components.

Wire up the AppRouter

Render AppRouter in your stripe.dashboard.fullpage view, and the router matches the URLs and renders the components:

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:

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:

<AppRouter
 context={context}
 redirectOnNotFound={{key: 'home'}}
/>

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 useNavigation() hook, available in any component beneath a NavigationProvider, returns two navigation mechanisms:

The two mechanisms serve different purposes: createAppRoute for declarative link-based navigation, and navigateToAppRoute for imperative programmatic navigation.

Use createAppRoute to build a route descriptor and pass it as the href prop to Link. This is the preferred approach for most navigation because it renders a real anchor element, giving users standard browser behaviour such as hover to preview the URL, right-click to open in a new tab, and accessible keyboard navigation.

createAppRoute accepts the same arguments as navigateToAppRoute – a route name and, for routes with dynamic segments, a parameters object:

const {createAppRoute} = useNavigation();

createAppRoute('home');
createAppRoute('customers');
createAppRoute('customer', {id: 'cus_123'});

The route that createAppRoute returns isn’t limited to the Link#href. Any component that accepts a route descriptor as a prop (such as DetailPage#breadcrumbs) works with it too. This lets you build navigable UI elements without imperative callbacks:

Imperative navigation with navigateToAppRoute

Not every navigation originates from a link. Use navigateToAppRoute 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:

Because of the type registration you set up earlier, TypeScript enforces that navigateToAppRoute('customer', {id: item.id}) includes the required id parameter, and that navigateToAppRoute('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.

Other viewports, such as stripe.dashboard.drawer.default, can link into your full-page routes.

Wrap the view in the same NavigationProvider, then call useNavigation() to build links or navigate programmatically:

Reuse the same routes config you defined for your full-page view so the links resolve to the correct URLs. createAppRoute('customers') returns a route descriptor that points at your full-page app’s /customers URL, so selecting the button takes the user into the full-page view.

Read the current route

Sometimes you need to know where you are rather than navigate somewhere else. The useAppRoute() hook, available in any component rendered inside AppRouter, returns the currently active app route. 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:

Read and write search parameters

A route’s path tells you which view to show, but an app URL also carries so-called search parameters that follow the ?. For example, these search parameters might capture the state defining how the current app view is configured: which tab is open, how a list is sorted, which filters are applied. Keep this state in the URL so users can bookmark and share it, but it rarely needs a route of its own.

https://dashboard.stripe.com/<ACCOUNT_ID>/app/<APP_ID>/my-customers?status=active&sort=recent
 └────────────┘└────────────────────────┘
 Route path Search parameters

The useAllSearchParams() hook reads and writes this state. It works like React’s useState, returning a [searchParams, setSearchParams] tuple:

Replace or merge

The setSearchParams function returned by useAllSearchParams lets you update the search parameters to a different value.

You can pass the next state of search parameters directly, or a function that calculates it from the previous state.

Pass the next state of search parameters directly when you need to replace the entire set of search parameters:

// URL: ?status=active&sort=recent
setSearchParams({status: 'archived'});
// URL: ?status=archived — sort is gone

Pass a function to update only certain search parameters and keep the rest:

// URL: ?status=active&sort=recent
setSearchParams((prev) => ({...prev, status: 'archived'}));
// URL: ?status=archived&sort=recent

Use the function form when a component updates some parameters while preserving others. It reads the latest values when it runs, so two controls don’t overwrite each other.

Read and write one search parameter

Use useSearchParam() when a component reads and writes only one search parameter. Pass the parameter’s name to the hook. It returns a [value, setValue] tuple and preserves all other search parameters when you update the value:

Like setSearchParams, the setter returned by useSearchParam accepts either the next value or a function that calculates it from the previous value. Pass undefined to remove the parameter.

Encoding and decoding

A URL can only hold text. This means that when you write a search parameter, its value is turned into text and added to the URL (encoded); when you read it, that text is turned back into a value (decoded). A value can be a string, null, undefined, an array or a nested object – but not a number or boolean.

setSearchParams({
 query: 'acme',
 tags: ['vip', 'overdue'],
 range: {from: '2024-01', to: '2024-03'},
 archived: undefined,
});

is encoded as:

?query=acme&tags[0]=vip&tags[1]=overdue&range[from]=2024-01&range[to]=2024-03

A few rules follow from this:

  • Arrays get numbered keys ( tags[0] , tags[1] ), so their order is kept.
  • Objects nest with bracketed keys ( range[from] ).
  • A value of undefined is left out, which is how you remove a parameter.
  • Nullish values, empty arrays or objects, and objects containing only null or undefined values are omitted from the search state.

Reading gives you the same structure back. Because a value can be a string, an array or an object, cast it to the shape you saved – and because everything is text, convert numbers yourself:

Note

In the Dashboard URL, all of the search parameters your app defines are stored under a special app key. The example below appears in the address bar as ?app[query]=acme&app[tags][0]=vip&app[tags][1]=overdue&app[range][from]=2024-01&app[range][to]=2024-03. This app prefix isn’t exposed to your app. Both hooks expose your app’s parameters without this prefix, so use top-level names such as query and tags directly.

const [searchParams] = useAllSearchParams();

const tags = (searchParams.tags as string[]) ?? [];
const page = parseInt(searchParams.page ?? '1', 10); // everything is text, so convert numbers

The createAppRoute function takes search parameters too, so a link can point at a route with the search parameters already applied. Pass a descriptor with key and searchParams:

Selecting the link opens the customers route with ?status=archived applied. This works anywhere a route descriptor is accepted, so you can preset a view from a breadcrumb, a table row or a menu item.

Updates replace the current history entry

By default, writing search parameters with either hook replaces the current URL rather than adding a browser history entry, so the back button returns the user to the route they came from. Set replace to false in the hook options to add a browser history entry instead.

Example: keep UI state in the URL

The following example keeps a set of selected language tags in the URL. Choose a few and observe how the search parameters are updated in the address bar.

Loading example...

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:

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

Last verified 2026-09-27

Is this helpful?