react-resource-view
Routing
The four primitives, the TanStack adapter, path mode and query mode.
Why a port at all#
The views navigate constantly — opening a record, switching layout, applying a filter — and every one of those is a URL change. Depending on a router would mean picking yours for you; instead the package asks for four primitives and stays out of it.
Unconfigured means full page loads
Left without a navigation port, the views fall back to the History API and full reloads. That is enough for a test or a story, and wrong for production.
The TanStack adapter#
import { configurePorts } from "react-resource-view"
import { tanstackAdapter } from "react-resource-view/tanstack"
configurePorts({ navigation: tanstackAdapter })Importing that entry point is what pulls TanStack Router in — the core never references it, so an application on another router installs nothing extra.
This site runs on it
Every demo you have clicked navigates through this adapter, on the same router that served the page you are reading.
Any other router#
configurePorts({
navigation: {
// Imperative navigation. Called as a hook.
useNavigate: () => {
const navigate = useRouterNavigate()
return ({ to, replace, resetScroll }) =>
navigate(to, { replace, preventScrollReset: !resetScroll })
},
// The current location, read reactively.
useLocation: () => {
const location = useRouterLocation()
return { pathname: location.pathname, searchStr: location.search }
},
Link: ({ to, children, ...rest }) => (
<RouterLink to={to} {...rest}>{children}</RouterLink>
),
Navigate: ({ to, replace }) => <RouterRedirect to={to} replace={replace} />,
},
})| Name | Type | Default | Description |
|---|---|---|---|
useNavigaterequired | () => (options: NavigateOptions) => void | Promise<void> | — | Called as a hook. resetScroll is false when switching tabs, so the reading position is kept — honour it if your router can. |
useLocationrequired | () => { pathname, searchStr, search? } | — | Read reactively, so a client-side navigation re-renders the views. searchStr is the raw query string. |
Linkrequired | ComponentType<LinkPropsInterface> | — | An anchor handled by the router. |
Navigaterequired | ComponentType<{ to, replace? }> | — | Redirects on render. |
Two ways to carry a context#
Path mode — the default
The context lives in the path, in a fixed order. It reads well, and it is what you want when the path is yours.
/{scope}/{resourceId}/{action}/{id}/{subResource}?filter=…
/admin/articles/list → the list
/admin/articles/list?filter=… → filtered
/admin/articles/read/42 → one article
/admin/articles/update/42 → editing it
/admin/articles/create?defaultData=… → a new one, pre-filledQuery mode
The whole context moves into a single query parameter. Two situations call for it:
- Static hosting. There is no server to answer
/admin/articles/read/42, so a deep link 404s. A query parameter hangs off a path that does exist. - Embedded views. The path belongs to the host page, not to the views.
configurePorts({
routing: {
mode: "query",
param: "view",
basePath: "/admin",
},
})
// → /admin?view=admin/articles/update/42&filter=…Reading is mode-agnostic
parseLink reads a URL carrying the routing parameter as query mode whatever the configuration says — so a link shared from a statically hosted page keeps working after you move to path mode.
This site uses query mode aimed at /playground: an action link inside a documentation demo has to leave the page it sits on, and it lands on the playground already on that record.
Mounting the views#
In path mode the views own a whole subtree, so one catch-all route is enough — the segments are parsed by the package, not by the router.
// TanStack Router — one catch-all under /admin
export const Route = createFileRoute("/admin/$")({
component: () => (
<ResourceViewProvider
viewResourceContextParams={parseLink(location.pathname + location.search)}
configuration={{ resources }}
/>
),
})ResourceViewProvider is the entry point when the views own the page: it resolves the scope, applies the configuration and renders the right view. ViewResourceContextProvider is the one to reach for when embedding a single view inside a page you control — that is what every demo on this site uses.
Building links#
Links elsewhere in the application — a menu, a notification, a dashboard card — should be built rather than written, so they follow the configured mode.
import { generateLink, generateLinkFromIri, generateLinkFromUri } from "react-resource-view"
// From a context
generateLink({ resourceId: "articles", resourceAction: ActionList.read, id: "42" })
// From an IRI you already hold
generateLinkFromIri({ iri: "/api/articles/42", resourceAction: ActionList.update })
// From an API URI, resolving which resource serves that path
generateLinkFromUri(notification.uri)| Name | Type | Default | Description |
|---|---|---|---|
generateLink | (params: ViewResourceContextParams) => string | — | The general form, in whichever mode is configured. |
generateLinkByResource | ({ resource, resourceAction, filter?, id? }) => string | — | The same, when you hold the resource object. |
generateLinkFromIri | ({ iri, resourceAction?, scope? }) => string | undefined | — | From an item IRI. |
generateLinkFromUri | (uri: string, scope?) => string | undefined | — | From an API URI, finding the resource whose path serves it — for turning a notification into a link. |
parseLink | (url: string) => ViewResourceContextParams | — | The inverse, in either mode. |