Capítulo 13 de 24

Chapter 13: Location Masking

Core Idea

Location masking lets a route render at one path internally while the browser's address bar shows a different path. This is the standard TanStack Router pattern for modal routes (a photo detail overlay) that should look like they never left the underlying list page, while still being deep-linkable and shareable at their "real" URL.

Setup

  • Routing style: code-based (createRoute / createRootRoute / createRouter, no file-based generation)
  • Key dependencies: @tanstack/react-router, @radix-ui/react-dialog (for the modal shell), @tanstack/react-router-devtools
  • Structure: single src/main.tsx defines a photo list route (/photos), a real detail route (/photos/$photoId), and a nested modal route (/photos/$photoId/modal) mounted under the list layout. The modal route is masked so its URL displays as the plain detail route.

Code Example

const routeTree = rootRoute.addChildren([
  photoRoute,
  photosLayoutRoute.addChildren([photoModalRoute]),
  indexRoute,
])

const photoModalToPhotoMask = createRouteMask({
  routeTree,
  from: '/photos/$photoId/modal',
  to: '/photos/$photoId',
  params: true,
})

// Set up a Router instance
const router = createRouter({
  routeTree,
  routeMasks: [photoModalToPhotoMask],
  defaultPreload: 'intent',
  scrollRestoration: true,
})

Linking into the masked route from the list page needs no special handling; the router applies the mask automatically because it is registered on routeMasks:

<Link
  to={photoModalRoute.to}
  params={{ photoId: photo.id }}
  // If you want to use a mask, you can do so like this, but
  // it's generally safer to set up a route mask instead.
  // mask={{ to: photoRoute.to, params: { photoId: photo.id } }}
>
  • What it demonstrates: createRouteMask maps a "from" route to a "to" URL shape (with params: true forwarding params), registered globally via routeMasks on createRouter so every navigation to the masked route is masked consistently without per-Link configuration.

Key Takeaways

  1. Prefer a global route mask (routeMasks on createRouter) over a per-Link mask prop; it is applied uniformly and is harder to forget on a new link.
  2. A masked route still resolves to its real component tree (here, the modal renders on top of the photo list, which stays mounted as the parent layout via photosLayoutRoute.addChildren).
  3. Opening the masked URL directly (e.g. "Open in new tab") de-masks it, showing the true nested route content (the plain detail page) instead of the modal, since there is no prior list page to overlay it on.
  4. Combine masking with errorComponent/pendingComponent on the masked route (PhotoModalErrorComponent, PhotoModalPendingComponent) so failure and loading states also render inside the modal shell, not the page.

Connects To

  • View Transitions (ch016): masked modal routes are a common pairing with view transitions, animating the overlay in/out while the underlying list stays visually stable.
  • Scroll Restoration (ch015): the photo list layout persists scroll position while the masked modal route mounts and unmounts on top of it, since navigating into/out of the modal doesn't remount the list.