Releases: lukemorales/next-safe-navigation
v0.4.0
Minor Changes
-
#29
cb2c4beThanks @mikerudge! - ### Standard Schema SupportThis release introduces support for Standard Schema, allowing you to use your favorite validation library for defining route params and search query schemas.
Previously,
next-safe-navigationwas tightly coupled withzod. Now, you can use anystandard-schemacompatible library, such asvalibot,arktype, and others, whilezodcontinues to be supported out-of-the-box.For existing users, your
zodschemas will continue to work without any changes.For new users or those wishing to switch, you can now use other libraries. For example, using
valibot:// src/shared/navigation.ts import { createNavigationConfig } from "next-safe-navigation"; import * as v from "valibot"; export const { routes, useSafeParams, useSafeSearchParams } = createNavigationConfig((defineRoute) => ({ customers: defineRoute("/customers", { search: v.object({ query: v.optional(v.string(), ""), page: v.optional(v.pipe(v.string(), v.transform(Number)), 1), }), }), // ... }));
v0.3.3
v0.3.2
Patch Changes
a953425Thanks @lukemorales! - Use iterator to checkURLSearchParamssize in older browsers by @wontondon
v0.3.1
Patch Changes
4f0e3a5Thanks @lukemorales! - Fix type definition foruseSafeParamswhen route has bothparamsandsearchParamsdefined
v0.3.0
Minor Changes
-
#15
31d794eThanks @lukemorales! - Add support forexperimental.typedRoutesYou may now enable
experimental.typedRoutesinnext.config.jsto have a better and safer experience with autocomplete when defining your routes
v0.2.0
Minor Changes
- #14
fc55e1dThanks @lukemorales! - Add better support for Catch-all Segments
Patch Changes
a5194b3Thanks @lukemorales! - Use regex instead of for loop to replace path params
v0.1.1
Patch Changes
be00799Thanks @lukemorales! - Fix route builder closure mutating same path string
v0.1.0
Minor Changes
-
8cbcb51Thanks @lukemorales! - Initial releaseStatic type and runtime validation of routes, route params and query string parameters on client and server components for navigating routes in NextJS App Router with Zod schemas.
[!WARNING]
Ensureexperimental.typedRoutesis not enabled innext.config.jsDeclare your application routes and parameters in a single place
// src/shared/navigation.ts import { createNavigationConfig } from "next-safe-navigation"; import { z } from "zod"; export const { routes, useSafeParams, useSafeSearchParams } = createNavigationConfig((defineRoute) => ({ home: defineRoute("/"), customers: defineRoute("/customers", { search: z .object({ query: z.string().default(""), page: z.coerce.number().default(1), }) .default({ query: "", page: 1 }), }), invoice: defineRoute("/invoices/[invoiceId]", { params: z.object({ invoiceId: z.string(), }), }), }));
Runtime validation for React Server Components (RSC)
[!IMPORTANT]
The output of a Zod schema might not be the same as its input, since schemas can transform the values during parsing (e.g.:z.coerce.number()), especially when dealing withURLSearchParamswhere all values are strings and you might want to convert params to different types. For this reason, this package does not expose types to inferparamsorsearchParamsfrom your declared routes to be used in page props:interface CustomersPageProps { // ❌ Do not declare your params | searchParam types searchParams?: ReturnType<typeof routes.customers.$parseSearchParams>; }
Instead, it is strongly advised that you parse the params in your server components to have runtime validated and accurate type information for the values in your app.
// src/app/customers/page.tsx import { routes } from "@/shared/navigation"; interface CustomersPageProps { // ✅ Never assume the types of your params before validation searchParams?: unknown } export default async function CustomersPage({ searchParams }: CustomersPageProps) { const { query, page } = routes.customers.$parseSearchParams(searchParams); const customers = await fetchCustomers({ query, page }); return ( <main> <input name="query" type="search" defaultValue={query} /> <Customers data={customers} /> </main> ) }; /* --------------------------------- */ // src/app/invoices/[invoiceId]/page.tsx import { routes } from "@/shared/navigation"; interface InvoicePageProps { // ✅ Never assume the types of your params before validation params?: unknown } export default async function InvoicePage({ params }: InvoicePageProps) { const { invoiceId } = routes.invoice.$parseParams(params); const invoice = await fetchInvoice(invoiceId); return ( <main> <Invoice data={customers} /> </main> ) };
Runtime validation for Client Components
// src/app/customers/page.tsx 'use client'; import { useSafeSearchParams } from "@/shared/navigation"; export default function CustomersPage() { const { query, page } = useSafeSearchParams('customers'); const customers = useSuspenseQuery({ queryKey: ['customers', { query, page }], queryFn: () => fetchCustomers({ query, page}), }); return ( <main> <input name="query" type="search" defaultValue={query} /> <Customers data={customers.data} /> </main> ) }; /* --------------------------------- */ // src/app/invoices/[invoiceId]/page.tsx 'use client'; import { useSafeParams } from "@/shared/navigation"; export default function InvoicePage() { const { invoiceId } = useSafeParams('invoice'); const invoice = useSuspenseQuery({ queryKey: ['invoices', { invoiceId }], queryFn: () => fetchInvoice(invoiceId), }); return ( <main> <Invoice data={invoice.data} /> </main> ) };
Use throughout your codebase as the single source for navigating between routes:
import { routes } from "@/shared/navigation"; export function Header() { return ( <nav> <Link href={routes.home()}>Home</Link> <Link href={routes.customers()}>Customers</Link> </nav> ) }; export function CustomerInvoices({ invoices }) { return ( <ul> {invoices.map(invoice => ( <li key={invoice.id}> <Link href={routes.invoice({ invoiceId: invoice.id })}> View invoice </Link> </li> ))} </ul> ) };