Frontend Routing APIs
<Link />
The <Link /> component is a drop-in replacement for <a /> tags, and is used to enable runtime page transitions via our routing system.
<Link href="/about">About</Link>The Link component is only a light wrapper around the a tag, and provides the following:
- Preloading of page data & components when hovering, via
router.preload(props.href) - When a user clicks, it calls
router.handleClickEvent(e, props.href)— which kicks off navigation, as long as the user isn’t holding Command/Ctrl, ensuring that ‘Open in new tab’ shortcuts work as expected. - Status attributes,
data-active,data-child-activeanddata-pendingviauseLinkState(props.href), allowing developers to style links based on their active or loading state. - In Gutenberg, link clicks are ignored — ensuring that authors don’t accidentally navigate to another page while selecting text.
You can prevent navigation by calling e.preventDefault() in the onClick prop.
It accepts the following props:
type LinkProps<T extends ElementType = "a"> = ComponentPropsWithRef<T> & {
/** Override the component/tag that gets rendered @default 'a' */
as?: T
/** The target URL */
href?: string | null
/** Target attribute */
target?: string | null
/** When set to 'true', the link will prefer to navigate back in history if possible — restoring scroll position */
preferBack?: boolean | "exact"
/** Additional data to attach to the route, which can be read by other components via `useRoute().linkData`, or in router events. */
linkData?: Record<string, any>
}<ScrollRestoration />
Since all navigation is handled by the stack, we need to take care of scrolling to the top of the page when a user clicks a link, and restoring the previous scroll position if the user clicks the browser back button.
The started theme includes <ScrollRestoration /> in _app.tsx with no props, however for sites with more complex animation/rendering/navigation, you can either remove this component entirely, or override the restoration function.
The default restore function looks something like this:
<ScrollRestoration
restore={(scroll) => {
// The `scroll` object has `left` and `top` values, and `direction` which is either `back` or `forward`.
document.scrollingElement?.scrollTo({
behavior: "instant",
left: scroll.left,
top: scroll.top,
})
}}
/>For more complex cases, you can build your own using useRestorableState(), or by hooking into the navigate:changed and capture-restorable-state route events.
<BackButton />
Display a back button that will navigate to the previous page in the router’s history.
This will allow you to render a ‘back button’ on the condition that the back button will not send the user to a different website. You can also specify an optional fallback URL, which will be used when the page is opened as a direct link — again, keeping the user on the site.
When a valid history route is available, the back button will function like history.go(-1). Otherwise, if a fallbackHref has been providing, clicking it will send the user to that page.
/** Always displays a back button, but will navigate to /work if the user hasn't navigated here from another page */
<BackButton
fallbackHref="/work"
render={(args) => {
return <button onClick={args.onClick}>Back</button>
}}
/>The BackButton component has the following props:
filter?: (route: RouteState) => boolean
type BackButtonProps = /**
* An optional matching function, which will stop the render button from appearing unless the function returns true.
*
* The below example will only render a back button if the last route was a project archive.
*
* eg. `filter={(route) => route.view === 'archive-projects'}`
*
* @param route The last route in the history stack.
*/
filter?: (route: RouteState) => boolean
/**
* Whether to use the top-most "global" route, or the "context" route.
*
* The "global" route is the top-most route, reflected in the URL bar.
*
* The "context" route will be the route that is currently being rendered, which may be different to the global route when using modals/overlays/route stacks/transitions.
*
* @default "context"
*/
mode?: "global" | "context"
/**
* An optional href, which will be used if no back route is found.
*
* When used, the back button will always render.
*/
fallbackHref?: string
/**
* A function to render the back button, if one should be rendered.
*
* Receives a `route` prop to further inspect the route, and an `onClick` function to navigate back.
*
* @param props
* @returns
*/
render: (props: { route: RouteState; onClick: (e?: any) => void }) => ReactNodeuseRoute()
This hook provides access to information about the current route. It returns an object with the following properties:
export type RouteState = {
/** A unique ID, representing this history item. */
id: string
/** A unique hash for this URI — useful for page transitions. */
key: string
/** The current path of the route eg. `"/about"` */
pathname: string
/** The un-parsed querystring, eg. `"?foo=bar"` */
search: string
/** The parsed querystring, parsed using the `qs` library, which supports arrays. */
query: Record<string, string | string[]>
/** The hash of the route, eg. `"#section"` */
hash: string
/** The normalised URI, composed from pathname, query and hash */
uri: string
/* An object that can hold transient state, like scroll position.
* When a user navigates back to a page, this state is included along with router events.
* By default, contains `scrollLeft` and `scrollTop`
* See `useRestorableState` for more info. */
returnState?: RouteReturnState
/** The name of the template, like `front-page` or `single-event` */
view: string
/** The view component for the current route. */
component: FunctionComponent
/** The props for this route, to be passed to the component. */
props: Props
/** Loaded metadata for this route */
meta: RouteMeta
/** Link data, provided using `<Link linkData={...} />`. Will be `undefined` if none was provided. */
linkData?: Record<string, any>
}
/** Data used for generation of <head /> tags */
type RouteMeta = {
title?: string
tags?: {
tagName: string,
attributes: Record<string, string>
inner?: string
}[]
}
Note that the view and props properties are fully typed, so you can extract props directly from a route by first checking it’s view property.
const route = useRoute()
let theme = 'default'
if (route.view === "single-project") {
theme = route.props.project.theme
}useViewRoute(name)
Similar to useRoute(), there’s also useViewRoute(viewName). It returns the same RouteState object as useRoute(), but it’ll only return the route data if the view matches. The return value will be fully typed, so you can access it’s props just like you would in the template.
const projectRoute = useViewRoute('single-project')
if (projectRoute) {
return <div>You're viewing the {project.props.project.title} project</div>
} else {
return <div>You're not viewing a project.</div>
}useRouter()
This hook provides access to the routing API, so that you can directly control navigation.
type RouterAPI = {
/** Preload a page and it's components */
preload: (url: string) => Promise<void>
/** Prefetch the data for a page, but not it's components. */
prefetch: (url: string) => Promise<void>
/** Begin transitioning to a new page */
navigate: (url: string, args?: { linkData?: Record<string, any> }) => Promise<void>
/** Replace the browser search params value with the given data, without transitioning the route */
replaceQuery: (search: Record<string, string | string[]>) => void
/** Replace the hash */
replaceHash: (hash: string) => void
/**
* Handle a click event, potentially triggering a route change.
*
* @param e A pointer or mouse event.
* @param href An optional URL. This must be provided if no `href` property exists on the clicked element
* @param preferBack If set, clicking this link will send the user 'back', when clicking a link to the previous history item.
*/
handleClickEvent(
e: PointerOrMouseEvent,
href?: string,
preferBack?: boolean | "exact",
element?: HTMLElement,
linkData?: Record<string, any>,
): void
/** A reference to the route loader (mostly for internal use) */
loader: RouteLoader
/** Subscribe to router events */
subscribe: (subscribe: RouterSubscriber) => () => void
/** Returns the current RouterAPIState, which contains the active route, history items, and pending navigation */
getState: () => RouterAPIState | null
/** Go back to a previous route in the history stack, or push it onto the stack if it isn't currently on the stack. */
restoreRoute: (route: RouteState) => void
}So, to manually trigger a route change, you can use something like:
const router = useRouter()
<button onClick={() => router.navigate("/")}>Go Home</button>useHydrating()
Formally called useIsSSR()
Use this function to help with SSR edge-cases, especially when relying on querystrings, hash fragments, user preferences, screen-size, random values or the current date/time — values which either aren’t available on the server, or will differ on the server.
On the server, this hook will always return true.
In the browser, this hook will return true for the first render while the page is hydrating, and then switch to false.
const isHydrating = useIsHydrating()
const hash = useRoute().hash
if (isHydrating) {
return <Spinner />
} else {
return <p>Hash is {hash}, and the time is {new Date().toString()}.</p>
}In the example above, the spinner will be rendered on the server, and for the first frame on the client, until the hash is available.
useRestorableState()
This hook works kinda like useState, and is used to store transient state that should be restored when a user navigates back to a page.
This is great if you have tabs or accordions which affect the scroll height of the page, which can be jarring when going forward and then back again.
The routing system uses this system internally for scroll restoration.
You’ll need to pass in an id, which acts as a key for the state, as well as a default value. The id should be unique for the current page, but can be reused across different pages.
/**
* The value will default to 0, but if the user changes the slider value,
* navigates away, and then comes back, the slider will be at the same value.
*
* If the user refreshes the page, the slider will reset back to 0.
*/
const [sliderValue, setSliderValue] = useRestorableState('sliderValue', 0)
return <div>
<input type="range" value={sliderValue} onChange={e => setSliderValue(e.valueAsNumber)} />
<Link href="/another-page">Another page</Link>
</div>useSearchParams()
This hook provides a nicer API for working with querystring parameters, and should be used when building search and filter interfaces.
const [params, api] = useSearchParams({
tags: [] as string[],
search: "",
})When calling the hook, you pass in an object of default values — which helps provide TypeScript hints, and ensures that required parameters are never null or undefined.
Simple arrays are supported, where api.set('tags', ['a', 'b']):{js} will result in a querystring of ?tags[]=a&tags[]=b.
Default values are managed individually — if a parameter is not set in the querystring, then it’ll be replaced with the default value.
Also, any parameters which are set back to their default value will be removed from the URL, to keep things tidy.
The hook returns a tuple of two objects:
- The returned
paramsobject contains the querystring parameters (arrays and strings) - The returned
apiobject provides functions for mutating the query state.
The api object has the following methods:
export type SearchParamAPI<T> = {
/**
* Set an individual parameter value by key.
* If the value is `null` or `undefined`, the parameter will be
* removed/set back to the default.
*/
set<K extends keyof T>(key: K, value: T[K]): void
/**
* Set all parameters at once, replacing the entire query string.
* Any parameters not present will be replaced with their default value.
* Passing an empty object is equivalent to calling `reset()`.
*/
setAll: (value: Partial<T>) => void
/**
* Returns a memoized setter function for a specific key.
* This is useful when you want to pass a setter function to a
* child component, without needing to pass the entire API object.
*
* For example:
* ```tsx
* <SearchInput value={params.q} onSearch={api.setter("q")} />
* ```
*
* @param key The search parameter name
* @returns (value: T) => void
*/
setter<K extends keyof T>(key: K): (value: T[K]) => void
/**
* Reset all parameters to their default values.
*/
reset(): void
/**
* Toggles an array item on or off.
*
* For example, `api.toggle("tags", "news")` will add "news" to the "tags" array if it isn't already present, or remove it if it is.
*
* Works well with `.has(key, item)`.
*/
toggle<K extends keyof Extract<T, Array<any>>>(
key: K,
item: Extract<T, Array<any>>[K] extends Array<infer U> ? U : never,
state?: boolean,
): void
/**
* Returns true/false depending on whether the item is present in the array parameter, or the value is equal.
*
* For example, `api.has("tags", "news")` will return true if "news" is present in the "tags" array.
*
* It can also be used as shorthand for api.get(key) === item, for non-array values.
*
* Works well with `.toggle(key, item)`.
*/
has<K extends keyof T>(key: K, item: Extract<T, Array<any>>[K] extends Array<infer U> ? U : T[K]): boolean
/**
* Gets the current value of a parameter.
* In most cases, you should use the `params` object returned from `useSearchParams` instead of this method.
*/
get<K extends keyof T>(key: K): T[K]
}Below is a more complete example, showing arrays and strings
const [params, api] = useSearchParams({
type: "all" as "all" | "news" | "events",
tags: [] as string[],
search: "",
})
return (
<>
{/* Search */}
<input type="search" defaultValue={params.search} onChange={(e) => api.set("search", e.currentTarget.value)} />
{/* Tags, using `has` and `toggle` */}
<ul>
{allTags.map((tag) => (
<label key={tag.value}>
<input
type="checkbox"
checked={api.has("tags", tag.value)}
onChange={(e) => api.toggle("tags", tag.value)}
/>
<span>{tag.label}</span>
</label>
))}
</ul>
{/* Type */}
<select value={params.type} onChange={(e) => api.set("type", e.currentTarget.value)}>
<option value="all">All</option>
<option value="news">News</option>
<option value="events">Events</option>
</select>
{/* Reset */}
<button onClick={() => api.reset()}>Reset</button>
</>
)Want to do something more advanced? Internally, this hook uses route.query and router.replaceQuery()
useLinkState()
This hooks provides insights on whether a link is currently loading, active, or if it’s children are active. It takes hashes and querystrings into account too.
It returns an object with the following structure:
type LinkState = {
active: 'path' | 'query' | 'exact' | undefined
pending: 'path' | 'query' | 'exact' | undefined
childActive: boolean
}This object is used by the Link component, to provide status attributes as data-active, data-child-active and data-pending.
Truthy values mean that at least the path is a match, but more specific values can help you figure out if the querystring and hash are also a match.
"path"means that the path is a match, but not the query/hash"query"means the path and query matches, but not the hash"exact"means the path, query and hash all match exactly
Advanced
useLinkProps()
This is used internally by the Link component, and accepts all the same props. It then returns the React props that you can add onto an a element — notably onMouseEnter/onPointerDown for preloading, and onClick for triggering the page navigation.
function FunnyLink(props: { url: string }) {
const linkProps = useLinkProps({ href: props.url })
return <a {...linkProps}>Click here</a>
}There’s almost no reason to use this, since all functionality is exposed via useRouter(). The router.handleClickEvent() function provides additional checks such link targets, file extensions and keyboard keys being held down.
useRouterState()
While the useRouter() hook provides access to the core navigational functionality of the router, and the useRoute() hook provides access to information about the current route, the useRouterState() hook provides access to the state of the router.
The most common use-case for this hook is to access the ‘history stack’ — that is, the list of routes that can be accessed via the browsers back button. This can be used alongside the <RouteRenderer /> component to keep past routes mounted, even while inactive.
You can also access the activeRoute to access the currently active route, normally accessed via useRoute() — however the activeRoute can be used to access the top-most active route, if multiple routes are being rendered.
The Router State object looks like this:
export type RouterAPIState = {
/** Router chain */
history: RouteState[]
/** The top-most, active route */
activeRoute: RouteState
/** The pending route */
pendingRoute?: RouteState
pendingRouteReady?: boolean
/** Transition blockers */
blockers: Promise<void>[]
}<NativeLinkHandler />
Renders a wrapper element, which handles link clicks from HTML <a /> inside it. This can be used when rendering HTML from somewhere else, where it’s expected that it may cotnain internal links.
<NativeLinkHandler as="div">
<div
dangerouslySetInnerHTML={{
// When the link is clicked, it'll work as though it was a <Link> element.
__html: `<a href="/about">About</a>`
}}>
</NativeLinkHandler><RouteDisplay />
The RouteDisplay component renders a route object. This is used internally by the router by default, with the result passed to your _app.tsx view via props.children — however it’s exposed for additional use-cases, like rendering historic routes in the background with the real route overlaid on top.
The following example shows how you could use this to make a simple modal system, by updating your _app.tsx to render routes manually. In the example, any link which opens a team profile, or has linkData={{openAsModal: true}} will open a modal overlay, but will still render them as normal pages when accessed via a direct link, or upon refresh.
function isModalRoute(route: RouteState) {
return route.linkData.openAsModal || route.view === "single-team"
}
export default defineView('_app', () => {
const pageRoutes = useRouterState((r) => {
return r.history.length === 1 ? r.history : r.history.filter((route) => !isModalRoute(route))
})
const modalRoute = useRouterState((r) => {
if (r.history.length <= 1) return null
const top = r.history[r.history.length - 1]
return isModalRoute(top) ? top : null
})
return <>
<div style={{ pointerEvents: modalRoute ? 'none' : 'auto' }}>
<header>
<Link href="/about">About</Link>
<Link href="/contact" linkData={{openAsModal: true}}>Contact Modal</Link>
<Link href="/team/daniel">Dan's Profile</Link>
</header>
<RouteDisplay route={pageRoutes[pageRoutes.length - 1]} />
<Footer />
</div>
{modalRoute && <div className="fixed inset-0 overflow-auto">
<BackButton render={({onClick}) => <button onClick={onClick}>Close</button>} />
<RouteDisplay route={modalRoute} />
</div>}
</>
})useRouterEvents()
The useRouterEvents() hook allows your components to listen to a large number of events, which are emitted by the router as the user interacts with a site.
You can use this to observe clicks, loading, navigation progress, and navigation errors. Some events can also be cancelled, allowing you to stop navigation when needed.
To use it, just pass an event handler:
useRouterEvents((e) => {
if (e.type === 'navigate:start') {
console.log('10% loaded...')
} else if (e.type === 'navigate:data-ready') {
console.log('60% loaded...')
} else if (e.type === 'navigate:will-change') {
console.log('80% loaded...')
} else if (e.type === 'navigate:changed') {
console.log('100% loaded!')
}
})Events
Below are all events emitted by the router, which can be monitored using useRouterEvents().
"clicked"
Triggered when a Link is clicked. It is NOT called when calling router.navigate(), but is called when calling router.handleClickEvent().
{
type: "clicked"
link: RouteLink
linkData: Record<string, any> | undefined
currentRoute: RouteState
// The clicked element
target?: HTMLElement
// ignore: The event was already cancelled, navigate: Navigation will occur, native: Likely a file download or a modifier key was held
mode: "ignore" | "navigate" | "native"
href: string | undefined
isSameUrl: boolean
cancel: () => void
}"capture-restorable-state"
Triggered when leaving a page. The state value can be mutated directly to ‘shelve’ data until an outgoing route, or update the scrollLeft/scrollTop properties which are automatically captured. It’s used by useRestorableState().
{
type: "capture-restorable-state"
state: RouteReturnState<Record<string, any>>
}"navigate:start"
Triggered when the navigation process is starting — just before the page starts to load. You can call .cancel() to prevent the transition.
{
type: "navigate:start"
link: RouteLink
linkData: LinkClickData | undefined
currentRoute: RouteState
hasPreloadedData: boolean
goingBack?: boolean
cancel: () => void
}"navigate:data-ready"
Triggered once the page data has been loaded, and the route object is ready. At this point, the actual React components needed by the page may not have loaded yet.
{
type: "navigate:data-ready"
link: RouteLink
linkData: LinkClickData | undefined
currentRoute: RouteState
loadedRoute: RouteState
}"navigate:will-change"
Triggered immediately before swapping to the new route. The React components still may not have loaded, but the Suspense transition is about to start.
{
type: "navigate:will-change"
link: RouteLink
linkData: LinkClickData | undefined
currentRoute: RouteState
loadedRoute: RouteState
}"navigate:changed"
Triggered once the new route has been fully loaded and rendered, and the transition is complete.
{
type: "navigate:changed"
link: RouteLink
linkData: LinkClickData | undefined
lastRoute: RouteState
currentRoute: RouteState
}"preload:start"
Triggered when preloading a page (eg. on hover)
{
type: "preload:start"
link: RouteLink
currentRoute: RouteState
}"preload:data-ready"
Triggered when page data has finished preloading (eg. on hover)
{
type: "preload:data-ready"
link: RouteLink
currentRoute: RouteState
data: RouteData
}"preload:finished"
Triggered when page data + the required React page template have finished preloading. Block components will not be loaded until this page is actually displayed.
{
type: "preload:finished"
link: RouteLink
currentRoute: RouteState
data: RouteData
}"error"
Called when an error occurs during either navigation or preloading.
If critical is true, then the JSON data of the page couldn't be loaded, and the _error` view is about to be rendered.
{
type: "error"
critical: boolean
linkData: LinkClickData | undefined
url?: string
shouldReload?: boolean
error: Error
}"error:component-load-failed"
Triggered when the view component couldn’t be loaded, which usually indicates a caching or network issue.
{
type: "error:component-load-failed"
error: Error
}