Skip to main content
Version: Next

Pages and routes

A page is a React component that Wasp renders as a full screen of your app. A route connects a URL path to a page. You declare both in your main.wasp.ts file and Wasp generates all the wiring for you, including lazy-loading each page's code by default. Internally, we use the industry-standard React Router to handle routing.

Declaring a page and a route​

To show a page at a URL, call route with a unique name, the URL path, and the page to render. Wrap your React component with page to turn it into a page:

main.wasp.ts
import { app, page, route } from "@wasp.sh/spec"
import { MainPage } from "./src/MainPage" with { type: "ref" }
import { AboutPage } from "./src/AboutPage" with { type: "ref" }

export default app({
// ...
spec: [
route("RootRoute", "/", page(MainPage)),
route("AboutRoute", "/about", page(AboutPage)),
],
})

The component is a regular React component, and it doesn't need to be exported in any particular way:

src/AboutPage.jsx
export function AboutPage() {
return (
<main>
<h1>About us</h1>
<p>We build things with Wasp.</p>
</main>
);
}

With this example, visiting /about would show your AboutPage component. Every route needs a unique name (in this case, "AboutRoute"), which you can use to build type-safe links between pages, as shown in Navigating between pages.

Wasp collects every route in the spec array and turns them into a single React Router configuration. If you have many pages, you can split the routes across several *.wasp.ts files.

Reading values from the URL​

Most apps have pages whose content depends on the URL, like /photo/42. Instead of declaring a route for every possible photo, put a dynamic segment in the path and read its value in the page.

Parameter segments​

Use :paramName in a route path to match any value in that segment. Access the matched value in your page component with the useParams hook from react-router:

main.wasp.ts
import { app, page, route } from "@wasp.sh/spec"
import { PhotoPage } from "./src/PhotoPage" with { type: "ref" }

export default app({
// ...
spec: [
route("PhotoRoute", "/photo/:photoId", page(PhotoPage)),
],
})
src/PhotoPage.jsx
import { useParams } from "react-router";

export function PhotoPage() {
const { photoId } = useParams();
return <div>Viewing photo {photoId}</div>;
}

Read more in the React Router docs on dynamic segments.

Optional segments​

Append ? to a path segment to make it optional. The route matches whether or not the segment is present:

main.wasp.ts
import { app, page, route } from "@wasp.sh/spec"
import { PhotoPage } from "./src/PhotoPage" with { type: "ref" }

export default app({
// ...
spec: [
route("PhotoRoute", "/photo/:photoId/edit?", page(PhotoPage)),
],
})
src/PhotoPage.jsx
import { useParams, useLocation } from "react-router";

export function PhotoPage() {
const { photoId } = useParams();
const { pathname } = useLocation();
const isEditing = pathname.endsWith("/edit");
return (
<div>
{isEditing ? "Editing" : "Viewing"} photo {photoId}
</div>
);
}

Read more in the React Router docs on optional segments.

Splats​

Use /* at the end of a route path to match any remaining path segments. Access the matched portion with the '*' param:

main.wasp.ts
import { app, page, route } from "@wasp.sh/spec"
import { FilesPage } from "./src/FilesPage" with { type: "ref" }

export default app({
// ...
spec: [
route("FilesRoute", "/files/*", page(FilesPage)),
],
})
src/FilesPage.jsx
import { useParams } from "react-router";

export function FilesPage() {
const { "*": filePath } = useParams();
// Visiting /files/docs/report.txt → filePath = "docs/report.txt"
return <div>File: {filePath}</div>;
}

Read more in the React Router docs on splats.

Query strings and hashes​

You don't need to declare query strings (?sortBy=date) or hashes (#comments) in the route path. Any page can read them with the useSearchParams and useLocation hooks from react-router:

src/TasksPage.jsx
import { useSearchParams } from "react-router";

export function TasksPage() {
const [searchParams] = useSearchParams();
const sortBy = searchParams.get("sortBy") ?? "name";
// Visiting /tasks?sortBy=date → sortBy = "date"
return <div>Tasks sorted by {sortBy}</div>;
}

To link from one page to another, use the Link component from wasp/client/router. It behaves the same as React Router's Link, with added types for route paths and parameters. If you give it a route path that doesn't exist, or a route with wrong parameters, you'll get a type error.

src/components/PhotoCard.jsx
import { Link } from "wasp/client/router";

export function PhotoCard({ photoId }) {
return (
<Link to="/photo/:photoId" params={{ photoId }}>
Photo {photoId}
</Link>
);
}

When you need to navigate from your own code instead of as a link (for example after a form submits), build the URL with the routes object. routes has one entry per route name from your spec, and you can pass it to React Router's useNavigate hook:

src/NewPhotoForm.jsx
import { useNavigate } from "react-router";
import { routes } from "wasp/client/router";

export function NewPhotoForm() {
const navigate = useNavigate();

async function handleSubmit() {
const photoId = await uploadPhoto(); // your upload logic
navigate(routes.PhotoRoute.build({ params: { photoId } }));
}

// ...
}

Use NavLink when the current page should be highlighted, or when you want to show a spinner during a pending transition. It behaves the same as React Router's NavLink, with added types for route paths and parameters. It is similar to Link, but className, style, and children can be render-prop functions that receive { isActive, isPending, isTransitioning }.

src/Navigation.jsx
import { NavLink } from "wasp/client/router";

export function Navigation() {
return (
<nav>
<NavLink
to="/tasks"
className={({ isActive }) =>
isActive ? "font-bold text-blue-600" : "text-gray-600"
}
>
Tasks
</NavLink>
</nav>
);
}

Restricting a page to logged-in users​

If your app uses authentication, you can mark a page with authRequired: true. Wasp then shows the page only to logged-in users and redirects everyone else to the path defined in auth.onAuthFailedRedirectTo:

main.wasp.ts
import { app, page, route } from "@wasp.sh/spec"
import { DashboardPage } from "./src/DashboardPage" with { type: "ref" }

export default app({
// ...
auth: {
// ...
onAuthFailedRedirectTo: "/login",
},
spec: [
route("DashboardRoute", "/dashboard", page(DashboardPage, { authRequired: true })),
],
})

A page with authRequired receives the logged-in user as the user prop:

src/DashboardPage.jsx
export function DashboardPage({ user }) {
return <h1>Hello, user {user.id}</h1>;
}

While Wasp checks whether the user is logged in, it shows a loading indicator instead of the page. Read more about the user object and about reading the current user from pages that are open to everyone in Accessing the logged-in user.

Sharing a layout between pages​

By default, Wasp renders each page on its own. To give every page a shared header, footer, or set of providers, declare a root component in the client config and render React Router's Outlet where the current page should go:

main.wasp.ts
import { app } from "@wasp.sh/spec"
import { Root } from "./src/Root" with { type: "ref" }

export default app({
// ...
client: {
rootComponent: Root,
},
})
src/Root.jsx
import { Outlet } from "react-router";

export function Root() {
return (
<div>
<header>My App</header>
<Outlet />
<footer>Made with Wasp</footer>
</div>
);
}

See Root Component for more details and examples.

Setting the page metadata​

The head field of your app config applies to every page.

But if you want to add metadata for only one of your pages, you can render <meta> elements inside the component. React moves them into the document's <head> for you:

src/PhotoPage.jsx
import { useParams } from "react-router";

export function PhotoPage() {
const { photoId } = useParams();
return (
<>
<meta name="description" content="A photo from my collection" />
<div>Viewing photo {photoId}</div>
</>
);
}

Read more in the React docs on <meta>, and in the SEO & GEO page.

Showing a page for not found URLs​

When a visitor opens a URL that no route matches, Wasp shows a generic error screen. To show your own "not found" page instead, add a route with a path of /*. The router picks the most specific matching route, so this route only renders when nothing else matches:

main.wasp.ts
import { app, page, route } from "@wasp.sh/spec"
import { NotFoundPage } from "./src/NotFoundPage" with { type: "ref" }

export default app({
// ...
spec: [
// ... your other routes
route("NotFoundRoute", "/*", page(NotFoundPage)),
],
})

Lazy-loaded routes​

By default, Wasp splits and lazy-loads all pages. This means that, for example, while the user is in the /about page, all the other pages' code is not loaded. And, when the user navigates to another page, that bundle of code is downloaded on-demand. This reduces the amount of data your users need to download and execute, and thus provides a faster initial load experience, similar to classic HTML sites. This is especially useful for apps with many routes.

In most apps, you won't notice lazy loading at all. The page's code is usually small and downloads quickly. But when a page has a lot of code, or your users are on slow connections, navigating to it can feel sluggish: the previous page stays on screen until the new page's code arrives. If a page needs to render instantly, like one users open right after landing on your app, set the lazy: false option on its route spec to include it in the initial bundle:

main.wasp.ts
import { app, page, route } from "@wasp.sh/spec"
import { DashboardPage } from "./src/DashboardPage" with { type: "ref" }

export default app({
// ...
spec: [
// This route's page will be included in the initial bundle
route("DashboardRoute", "/dashboard", page(DashboardPage), { lazy: false }),
],
})
caution

Disabling lazy loading means that this page's code will always be downloaded by the user's browser ahead of time. This will increase the initial load time of your app, especially if the page has a lot of code.

Prerendered routes​

You can prerender specific routes at build time by setting the prerender property. This generates static HTML that is served immediately, giving faster load times and better SEO.

main.wasp.ts
import { app, page, route } from "@wasp.sh/spec"
import { LandingPage } from "./src/LandingPage" with { type: "ref" }

export default app({
// ...
spec: [
route("LandingRoute", "/", page(LandingPage), { prerender: true }),
],
})

See the Prerendering page for the full documentation.

API Reference​

page and route specifications​

API reference

page »

All the options for declaring a page in the Wasp spec.

API reference

route »

All the options for declaring a route in the Wasp spec.

The 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)

    • An object with keys and values for each param in the path.
    • For example, if the path is /task/:id, then the params prop must be { id: 1 }. Wasp supports required and optional params.
  • search: Record<string, string>

    • An object with keys and values for each search param.
    • For example, the object { sortBy: 'date' } becomes ?sortBy=date.
  • hash: string

  • All other props that the react-router's Link component accepts

The NavLink component accepts the same to, params, search, and hash props as the Link component, plus:

  • All other props that the react-router's NavLink component accepts

    The most useful ones are:

    • className, style, and children

      • Besides regular values, these can be functions. NavLink calls them with an object describing the link's state and uses what they return. The object has these fields:
        • isActive: the link points to the current page.
        • isPending: the user clicked the link, and the page is still loading.
        • isTransitioning: a view transition to the page is in progress.
    • end: boolean

      • By default, a link is also active on nested paths. For example, a link to /tasks is active on /tasks/123. Set end to only make it active on its exact path.
    • caseSensitive: boolean

      • By default, the link ignores letter case when checking if it's active. Set caseSensitive to take case into account.

routes Object​

The routes object contains a function for each route in your app.

router.tsx
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.

React Router API​

Inside page components, you can use most navigation-related hooks and components from react-router, such as useParams, useSearchParams, useLocation, useNavigate, and Outlet.

Wasp defines the routes for you, so there's no way to attach React Router loaders or actions to them. APIs that depend on those, like useLoaderData, useActionData, useFetcher, or submitting a <Form> to an action, won't work. Use Wasp Operations to fetch and change data instead.