Search/Route Params- App
Versions
While the apis for app
and pages
are very similar, there are some important differences.
Make sure you follow the docs for your use case
This page: app
version
View the pages
version here
Server Components
Forced Dynamic Rendering
In page.tsx
, search params are accessible through props on the top level exported component. However, accessing search params in this way will force you into dynamic rendering (SSR). This is a behavior enforced by Next (see the “good to know” section at the very bottom)
If you do not want this behavior (i.e. you want some part of your page to be statically generated at build time or ISR’d), you are forced to place the search param logic in a client component. Check out the ‘Client Components’ section below to see more.
Usage in page.tsx
withParamValidation
next-typesafe-url/app/hoc
provides a higher order component withParamValidation
you can wrap your page with to provide runtime validation through your zod validator.
The InferPagePropsType
helper type is passed RouteType
as a generic to extrapolate the valid types coming out of the zod validator.
Note: If no error is thrown, your PageProps will always be the types you defined in your schema.
Note: If your page does not consume any search/route params (i.e. it is a ‘static’ route), there is no need to use withParamValidation
// app/product/[productID]/page.tsx
import { withParamValidation } from "next-typesafe-url/app/hoc";
import type { InferPagePropsType } from "next-typesafe-url";
import { Route, type RouteType } from "./routeType";
type PageProps = InferPagePropsType<RouteType>;
async function Page({ routeParams, searchParams }: PageProps) {
return (
<>
<div>{JSON.stringify(routeParams)}</div>
<div>{JSON.stringify(searchParams)}</div>
</>
);
}
export default withParamValidation(Page, Route);
Errors
If the zod validation fails, withParamValidation
will throw a ZodError
. Use Next’s error.tsx
to handle these thrown errors.
Usage in layout.tsx
Layouts only have access to route params, not search params (see why).
In terms of validation, a layout could represent any number of routes within it, all of which may have their own validators which may not neccesarily overlap.
Because of this, you must define a new zod validator for each layout, which accurately represents the union of all possible valid route params for all nested routes.
withLayoutParamValidation
next-typesafe-url/app/hoc
provides a higher order component withLayoutParamValidation
you can wrap your layouts with to provide runtime validation through your zod validator.
The InferLayoutPropsType
helper type is passed the type of your LayoutRoute as a generic to extrapolate the valid types coming out of the zod validator.
Note: If no error is thrown, your PageProps will always be the types you defined in your schema.
Note: If your layout does not consume any route params, there is no need to use withLayoutParamValidation
// app/product/[productID]/layout.tsx
import { z } from "zod";
import { withLayoutParamValidation } from "next-typesafe-url/app/hoc";
import type { DynamicLayout, InferLayoutPropsType } from "next-typesafe-url";
const LayoutRoute = {
routeParams: z.object({
productID: z.number(),
}),
} satisfies DynamicLayout;
type LayoutType = typeof LayoutRoute;
type Props = InferLayoutPropsType<LayoutType>;
async function Layout({ children, routeParams }: Props) {
return (
<div>
<p>{JSON.stringify(routeParams)}</p>
<div>{children}</div>
</div>
);
}
export default withLayoutParamValidation(Layout, LayoutRoute);
Errors
If the zod validation fails, withLayoutParamValidation
will throw a ZodError
. Use Next’s error.tsx
to handle these thrown errors.
Client Components
If you don’t have a specific reason to need to access params on the Client, it is recommended to use the server patterns found in the “Server Components” section above, but this is just general advice.
Hooks
next-typesafe-url/app
exports a useRouteParams
and useSearchParams
hook that will return the route params / search params for the current route. They take one argument, the zod schema for either route params or search params from the Route object.
import { Route } from "~/app/product/[productID]/routeType.tsx";
import { useSearchParams, useRouteParams } from "next-typesafe-url/app";
function Component() {
const routeParams = useRouteParams(Route.routeParams);
const searchParams = useSearchParams(Route.searchParams);
const { data, isLoading, isError, error } = searchParams;
if (isLoading) {
return <div>loading...</div>;
} else if (isError) {
return <div>Invalid search params {error.message}</div>;
} else {
return <div>{data.userInfo.name}</div>;
}
}
Errors
isLoading
is the loading state of the internal Next router, and isError
is a boolean that is true if the params do not match the schema. If isError
is true, then error
will be a ZodError
object you can use to get more information about the error. (also check out zod-validation-error to get a nice error message)
If isLoading
is false and isError
is false, then data
will always be valid and match the schema.
Note: Be mindful when using useSearchParams
and useRouteParams
in components used in multiple routes, making sure you pass the correct validator
IMPORT WARNING
next-typesafe-url/pages
ALSO exports a useRouteParams
and useSearchParams
hook, but these are NOT the same as the hooks exported from next-typesafe-url/app
.
MAKE SURE YOU IMPORT FROM THE RIGHT PATH
Advanced Routing Patterns
Parallel Routes
page.tsx
in any parallel routes should use the import from the routeType.ts
from the parent directory
This is because they will be shown on the same route, receiving the same route params and search params, and therefore should use the same zod validator.
app
├── @analytics
└── page.tsx <- THIS SHOULD IMPORT FROM |
└── layout.tsx |
└── page.tsx |
└── routeType.ts <-----------------------------
Adjusting Layout Props
InferLayoutPropsType
takes an optional second generic of string
or a union of string
s (for multiple parallel routes) that should represent any parallel routes beneath the layout.
type Props = InferLayoutPropsType<LayoutType, "analytics">;
function Layout({ children, routeParams, analytics }: Props) {
return (
<div>
<p>{JSON.stringify(routeParams)}</p>
<div>{children}</div>
<div>{analytics}</div>
</div>
);
}
export default withLayoutParamValidation(Layout, LayoutRoute);
Intercepted Routes
Like parallel routes, page.tsx
in any intercepted routes should import from the routeType.ts
from the directory of the route being intercepted
This way, whether that route is accessed directly or intercepted, the same validation is used.
app
├── @modal
└── (.)foo
└── page.tsx <- THIS SHOULD IMPORT FROM |
└── layout.tsx |
└── foo |
└── page.tsx |
└── routeType.ts <-----------------------------