Routing hooks
useLocation, useParams, useNavigate, usePrefetch, useLoaderData — navigating + reading URL state.
The routing hooks from @voltro/web. They read URL state, trigger navigation, and access loader data.
useLocation()
The current pathname (just the path; query string is separate).
import { useLocation } from '@voltro/web'
const pathname = useLocation()
// '/docs/intro/getting-started'Updates on every navigation. Use for:
- Active-link styling
- "Did the URL change?" effect dependencies
- Conditional rendering based on path
const Nav = () => {
const pathname = useLocation()
return (
<ul>
<li><Link to="/" className={pathname === '/' ? 'active' : ''}>Home</Link></li>
<li><Link to="/about" className={pathname === '/about' ? 'active' : ''}>About</Link></li>
</ul>
)
}For "active if URL starts with prefix":
className={pathname.startsWith('/dashboard') ? 'active' : ''}useParams<T>()
URL params from [name] segments. Typed via the generic.
// src/pages/users/[id].tsx
import { useParams } from '@voltro/web'
const { id } = useParams<{ id: string }>()For catch-all queries:
// src/pages/docs/[...slug].tsx
const { slug } = useParams<{ slug: string }>()
// /docs/intro/getting-started → slug = "intro/getting-started"For multi-segment dynamic paths:
// src/pages/orgs/[orgId]/projects/[projectId].tsx
const { orgId, projectId } = useParams<{ orgId: string; projectId: string }>()Params are always strings — convert numbers explicitly:
const id = Number(params.id)
if (Number.isNaN(id)) throw new NotFoundError()useNavigate()
Programmatic navigation.
import { useNavigate } from '@voltro/web'
const SignOutButton = () => {
const navigate = useNavigate()
const onSignOut = async () => {
await fetch('/auth/signout', { method: 'POST' })
navigate('/login')
}
return <button onClick={onSignOut}>Sign out</button>
}Returns a function (to: string, options?) => void.
| Option | Notes |
|---|---|
replace: true |
Replace the history entry (no back-button entry). |
scroll: false |
Don't scroll to top after navigation. |
navigate takes a path string only — there is no numeric history overload. For back / forward, reach for the browser API:
window.history.back() // back
window.history.forward() // forwarduseBlocker()
Hold a pending navigation so you can prompt before the user leaves — the unsaved-changes guard.
import { useBlocker } from '@voltro/web'
const blocker = useBlocker(form.isDirty) // boolean or a predicate
// …
{blocker.blocked && (
<ConfirmDialog onConfirm={blocker.retry} onCancel={blocker.reset} />
)}Pass true/false or a predicate ({ to, opts }) => boolean (to allow some destinations). When a navigation is held, the hook returns { blocked: true, to, retry, reset }: retry() proceeds, reset() cancels. A full-page unload also triggers the browser's native prompt while any blocker is active. See Navigation for the full example.
useSearchParams() + useSetSearchParams()
Read the query string as a URLSearchParams (SSR-aware — the request URL on the server, window.location.search on the client):
import { useSearchParams } from '@voltro/web'
const tab = useSearchParams().get('tab') ?? 'overview'Write it with useSetSearchParams() — the setter updates the query via navigate, so readers re-render immediately:
import { useSetSearchParams } from '@voltro/web'
const setParams = useSetSearchParams()
setParams({ tab: 'members' }) // set (default: replace)
setParams((p) => { p.set('page', '2'); return p }) // patch one param
setParams({ page: '2' }, { push: true }) // distinct history entryWrites default to a history replace; pass { push: true } for a Back entry or { scroll: false } to keep scroll. See Navigation.
usePrefetch()
Trigger loader-data prefetch on hover / focus. Wired automatically by <Link prefetch />; export only useful for custom triggers.
import { usePrefetch } from '@voltro/web'
const Card = ({ id }) => {
const prefetch = usePrefetch()
return (
<article onMouseEnter={() => prefetch(`/notes/${id}`)}>
{/* …card body, no Link inside */}
</article>
)
}Idempotent — multiple calls for the same path fire one loader. The prefetched result is held in the loader cache until it's consumed by the actual navigation (or invalidated on an error reset).
useLoaderData<T>()
Page's loader output, typed.
// src/pages/notes/[id].tsx
import { useLoaderData } from '@voltro/web'
interface Note {
id: string
title: string
}
export const loader = async ({ params }): Promise<Note> => {
return await fetchNote(params.id)
}
export default function NotePage(): ReactNode {
const note = useLoaderData<Note>()
return <article><h1>{note.title}</h1></article>
}Available in:
- The page component itself
- Any layout in the page's chain
- Any descendant of the layout
Returns null on pages without a loader. The generic narrows the type.
Precisely, it returns LoaderData<T>. For every ordinary loader that IS T. For
a loader that returned defer(), LoaderData<T> flattens the two buckets into
one object — eager fields as values, deferred fields as Promise<T> — so the
compiler tells you which fields have to be rendered through
<Await>:
export const loader = async ({ query }) => defer(
{ user: await query('users.me') },
{ report: query('reports.quarterly') },
)
// user: User report: Promise<Report>
const { user, report } = useLoaderData<Awaited<ReturnType<typeof loader>>>()If you wrap this hook in your own generic helper, propagate the mapped type
(<D,>(): LoaderData<D> => useLoaderData<D>()) — LoaderData<D> is not
assignable to a bare type parameter D.
See Loaders & meta for the server-side counterpart.
Compositions
// "go to next page after a delay"
const navigate = useNavigate()
useEffect(() => {
const t = setTimeout(() => navigate('/welcome'), 3000)
return () => clearTimeout(t)
}, [navigate])// "save the URL the user came from for after-signin redirect"
const pathname = useLocation()
const fromUrl = useMemo(() => pathname, []) // capture at mount, not every render// "active link with hover-prefetch"
const Link = ({ to, children }) => {
const pathname = useLocation()
const prefetch = usePrefetch()
const active = pathname === to
return (
<a
href={to}
onMouseEnter={() => prefetch(to)}
className={active ? 'active' : ''}
>
{children}
</a>
)
}
// (You don't usually write this — `<Link>` from `@voltro/web` does it all.)Anti-patterns
window.location.href = '/foo'for internal nav. Full reload — defeats the SPA. UseuseNavigate.- Reading
paramsoutside the page tree. Layouts above the page CAN read params (they share the chain), but components imported as siblings can't. Pass params down explicitly. useEffect(() => navigate(...), [])for default redirects. Triggers a flash of the original page. Do redirects in the LOADER instead (throw new RedirectError(...)).
See also
- Navigation —
<Link>+ prefetch patterns - Loaders & meta — what populates
useLoaderData