Concepts
All navigation state lives in the URL: a view, a panel, a modal and a popout are TanStack Router routes. The host’s back button, a reload and a deep link restore the screen with no separate navigation store.
Feature parity, not API parity
Section titled “Feature parity, not API parity”@vkma.dev/router succeeds @vkontakte/vk-mini-apps-router: every capability of the old router is here, but the API is different. String panel ids are replaced by typed handles, the object route config by builders, and TanStack Router does the matching underneath. Coming from the old package? Start with the migration guide.
Four layers
Section titled “Four layers”One package, four subpath exports. The layers point strictly downward:
| Import | Layer |
|---|---|
@vkma.dev/router |
React-free core: createVkHistory, createBridgeSync, createSwipeBackManager |
@vkma.dev/router/react |
data hooks with freeze semantics: usePanelParams and friends, usePanelsHistory |
@vkma.dev/router/vkui |
VKUI hosts: VkView, Modals, Popouts, NotFound, ModalPageSurface |
@vkma.dev/router/preset |
createApp + defineView/definePanel, useNavigator, createVkLink, sugar |
Two rules keep the layers clean:
- VKUI never leaks below
/vkui— the core runs in any React setup, with or without VKUI. - The bridge is a structural
{ send, subscribe }: both@vkma.dev/bridgeand@vkontakte/vk-bridgefit; there is a hard dependency on neither.
Import from /preset when you build a VKUI app — the layer re-exports everything below it. The lower subpaths matter when you consume only part of the package.
Additive over TanStack
Section titled “Additive over TanStack”TanStack is a peer dependency, and the package re-invents nothing on top of it. Route options (loader, beforeLoad, validateSearch, lazy, pendingComponent, …) pass into the routes untouched: everything TanStack Router does keeps working here. Its documentation applies to data and validation — the bridge and router packages answer different questions.
The navigation surface
Section titled “The navigation surface”Everything the old router drove with string ids converges into two tools:
useNavigator()— push/replace/back/backToFirst,showModal/showGlobalModal/hideModal,showPopout/hidePopoutandblock. The hook subscribes to no router state: what it needs is read from the liverouter.stateat call time, so using the hook triggers no re-renders and the navigator object has a stable identity.createVkLink(CellButton)— turns any component into a typed link: the targets are the same Page objectsnavigator.pushaccepts.
Both take panel handles instead of strings — see views and panels.
Static children instead of Outlet
Section titled “Static children instead of Outlet”The /vkui layer renders every panel of a view as an always-mounted VKUI View child; the router only feeds activePanel. Two consequences look like bugs but are inherent VKUI behavior:
- VKUI infers the animation direction from the declaration order of the children — a push that skips panels back animates BACK;
replacestill animates.
Details in views and panels.
Freeze semantics of the data hooks
Section titled “Freeze semantics of the data hooks”A panel stays mounted during the back animation while its route no longer matches — raw TanStack hooks (useParams, useLoaderData) throw at that moment. Use the package hooks — usePanelParams, usePanelSearch, usePanelLoaderData: they never throw and retain the last value while the panel stays mounted.
- Views and panels —
defineView, animation order, loaders. - Modals and popouts — overlays as routes.
- History and the host — URL modes,
VKWebAppSetLocation, swipe-back. - Migration — from
@vkontakte/vk-mini-apps-router.