web/docs/advanced/links.md
import { Required } from '@site/src/components/Tag'
If you are using Typescript, Wasp gives you typesafe building blocks for navigation. You get autocompletion on route paths, compile errors when params are missing, and a single source of truth between your main.wasp.ts file and your client code.
For navigating between pages inside JSX, Wasp exposes two components from wasp/client/router: Link for simple links, and NavLink when you need to react to navigation state.
LinkReach for Link when you just need to send the user to another page. Given this route:
import { app, page, route } from "@wasp.sh/spec"
import { TaskPage } from "./src/TaskPage" with { type: "ref" }
export default app({
// ...
spec: [
route("TaskRoute", "/task/:id", page(TaskPage)),
],
})
You'd use it like this:
import { Link } from "wasp/client/router"
export const TaskList = () => {
// ...
return (
<div>
{tasks.map((task) => (
<Link
key={task.id}
to="/task/:id"
params={{ id: task.id }}>
{task.description}
</Link>
))}
</div>
)
}
The to prop is autocompleted from the routes you defined in main.wasp.ts, and params is typechecked against the path you picked. Rename a route or change a param, and any broken Link is pointed out by Typescript.
NavLinkUse NavLink when the current page should be highlighted, or when you want to show a spinner during a pending transition. It takes the same props as Link, but className, style, and children can be render-prop functions that receive { isActive, isPending, isTransitioning }.
import { NavLink } from "wasp/client/router"
export const Navigation = () => {
return (
<nav>
<NavLink
to="/tasks"
className={({ isActive }) =>
isActive ? "font-bold text-blue-600" : "text-gray-600"
}
>
Tasks
</NavLink>
</nav>
)
}
Everything below applies to both Link and NavLink.
If a route path ends with a /* pattern (also known as splat), pass the rest of the path as the * param:
import { app, page, route } from "@wasp.sh/spec"
import { CatchAllPage } from "./src/CatchAllPage" with { type: "ref" }
export default app({
// ...
spec: [
route("CatchAllRoute", "/pages/*", page(CatchAllPage)),
],
})
<Link to="/pages/*" params={{ "*": "about" }}>
About
</Link>
This renders as /pages/about.
If a route has an optional static segment, you can choose at the call site whether to include it or not:
import { app, page, route } from "@wasp.sh/spec"
import { OptionalPage } from "./src/OptionalPage" with { type: "ref" }
export default app({
// ...
spec: [
route("OptionalRoute", "/task/:id/details?", page(OptionalPage)),
],
})
/* You can include the optional segment ... */
<Link to="/task/:id/details" params={{ id: 1 }}>
Task 1
</Link>
/* ... or leave it out */
<Link to="/task/:id" params={{ id: 1 }}>
Task 1
</Link>
You can also pass search and hash to attach a query string and fragment:
<Link
to="/task/:id"
params={{ id: task.id }}
search={{ sortBy: "date" }}
hash="comments"
>
{task.description}
</Link>
This renders as /task/1?sortBy=date#comments. Check out the API Reference for the full list of accepted props.
When you need a URL string instead of a component, for example for useNavigate, redirects, window.location, or anywhere you are not rendering JSX, use the routes object from wasp/client/router:
import { routes } from "wasp/client/router"
const linkToTask = routes.TaskRoute.build({ params: { id: 1 } })
linkToTask is the string /task/1. Each route from main.wasp.ts shows up on routes with a build function whose options are typed against the route's path, so the same compile-time safety you get from Link is also available outside of JSX.
build follows the same rules as the components above: catch-all routes take a * param, optional static segments pick a concrete path, and you can attach a query string and fragment via search and hash.
const linkToTaskComments = routes.OptionalRoute.build({
path: "/task/:id/details",
params: { id: 1 },
search: { sortBy: "date" },
hash: "comments",
})
This renders as /task/1/details?sortBy=date#comments. Check out the API Reference for the full shape.
Link ComponentThe Link component accepts the following props:
to <Required />
A valid Wasp Route path from your main.wasp.ts file.
In the case of optional static segments, you must provide one of the possible paths which include or exclude the optional segment. For example, if the path is /task/:id/details?, you must provide either /task/:id/details or /task/:id.
params: { [name: string]: string | number } <Required /> (if the path contains params)
/task/:id, then the params prop must be { id: 1 }. Wasp supports required and optional params.search: string[][] | Record<string, string> | string | URLSearchParams
URLSearchParams constructor.{ sortBy: 'date' } becomes ?sortBy=date.hash: string
all other props that the react-router's Link component accepts
NavLink ComponentThe NavLink component accepts the following props:
to <Required />
A valid Wasp Route path from your main.wasp.ts file.
In the case of optional static segments, you must provide one of the possible paths which include or exclude the optional segment. For example, if the path is /task/:id/details?, you must provide either /task/:id/details or /task/:id.
params: { [name: string]: string | number } <Required /> (if the path contains params)
/task/:id, then the params prop must be { id: 1 }. Wasp supports required and optional params.search: string[][] | Record<string, string> | string | URLSearchParams
URLSearchParams constructor.{ sortBy: 'date' } becomes ?sortBy=date.hash: string
all other props that the react-router's NavLink component accepts
className, style, and children accept render-prop functions that receive { isActive, isPending, isTransitioning }, and end and caseSensitive control how the active match is computed.routes ObjectThe routes object contains a function for each route in your app.
export const routes = {
// RootRoute has a path like "/"
RootRoute: {
build: (options?: {
search?: string[][] | Record<string, string> | string | URLSearchParams
hash?: string
}) => // ...
},
// DetailRoute has a path like "/task/:id/:userId?"
DetailRoute: {
build: (
options: {
params: { id: ParamValue; userId?: ParamValue; },
search?: string[][] | Record<string, string> | string | URLSearchParams
hash?: string
}
) => // ...
},
// OptionalRoute has a path like "/task/:id/details?"
OptionalRoute: {
build: (
options: {
path: "/task/:id/details" | "/task/:id",
params: { id: ParamValue },
search?: string[][] | Record<string, string> | string | URLSearchParams
hash?: string
}
) => // ...
},
// CatchAllRoute has a path like "/pages/*"
CatchAllRoute: {
build: (
options: {
params: { "*": ParamValue },
search?: string[][] | Record<string, string> | string | URLSearchParams
hash?: string
}
) => // ...
},
}
The params object is required if the route contains params. The search and hash parameters are optional.
You can use the routes object like this:
import { routes } from "wasp/client/router"
const linkToRoot = routes.RootRoute.build()
const linkToTask = routes.DetailRoute.build({ params: { id: 1 } })
const linkToOptional = routes.DetailRoute.build({
path: "/task/:id/details",
params: { id: 1 },
})
const linkToCatchAll = routes.CatchAllRoute.build({
params: { "*": "about" },
})