| name | ns-intercepted-route-using-tanstack-start |
| description | Implement masked modal routes in NowStack Mobile web-app. Use for TanStack Start route masks, shareable detail URLs, back-to-close, and refresh-to-page behavior. |
Intercepted Route Using TanStack Start
Implement a web detail flow that opens as a modal during client navigation while the browser URL shows the real detail route. Refreshing or directly visiting that URL must render a normal full page.
This skill applies only to `web-app/`.
Current route shape:
- Public:
/, /privacy, /cgv, /support
- User app:
/app
- Admin:
/admin (redirects to /admin/users), /admin/users, /admin/users/$userId
The examples below use a hypothetical /admin/items list/detail flow to illustrate the pattern.
For an admin items list/detail flow:
- User starts on
/admin/items.
- User clicks a row.
- Browser URL becomes
/admin/items/$itemId.
- UI keeps the
/admin/items list visible and opens a modal/sheet with the detail.
- Back or close returns to
/admin/items.
- Refresh or direct visit on
/admin/items/$itemId renders the full detail page.
<core_pattern>
Navigate to the source route internally, store modal state in source route search, and mask the visible URL as the destination route.
<Link
to="/admin/items"
search={{ itemId: item._id }}
mask={{
to: "/admin/items/$itemId",
params: { itemId: item._id },
unmaskOnReload: true,
}}
resetScroll={false}
>
View
</Link>
The source route owns modal state:
type ItemsSearch = {
itemId?: string;
};
export const Route = createFileRoute("/admin/items")({
validateSearch: (search: Record<string, unknown>): ItemsSearch => ({
itemId:
typeof search.itemId === "string" && search.itemId.length > 0
? search.itemId
: undefined,
}),
component: AdminItemsPage,
});
The destination route remains a real route:
export const Route = createFileRoute("/admin/items/$itemId")({
component: AdminItemDetailPage,
});
</core_pattern>
Use `location.maskedLocation` before deciding whether close should call `history.back()`.
const router = useRouter();
const location = useLocation();
const closeModal = () => {
if (location.maskedLocation) {
router.history.back();
return;
}
void router.navigate({
to: "/admin/items",
search: {},
replace: true,
});
};
If the modal has an "Open full page" action, navigate directly and do not call the close handler first:
void router.navigate({
to: "/admin/items/$itemId",
params: { itemId },
replace: true,
});
- Keep the destination route real and directly visitable.
- Use `mask={{ to: destination, params, unmaskOnReload: true }}`.
- Keep modal state in internal route search, not visible `?modal=...` URLs.
- Close with `history.back()` only when `location.maskedLocation` exists.
- Do not call close immediately before navigating to the full page.
- Preserve auth/admin route guards already present in `web-app/app/routes/admin/route.tsx`.
Run:
cd web-app && npm run typecheck
cd web-app && npm run build
Then verify manually or with browser automation:
- Open source route.
- Click detail link.
- Confirm browser URL is the detail URL.
- Confirm modal/sheet is visible over source context.
- Use Back or close and confirm source route returns.
- Open detail URL directly or refresh and confirm full detail page renders.