Skip to content

Releases: lukemorales/next-safe-navigation

v0.4.0

24 Jun 16:08
8adf803

Choose a tag to compare

Minor Changes

  • #29 cb2c4be Thanks @mikerudge! - ### Standard Schema Support

    This 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-navigation was tightly coupled with zod. Now, you can use any standard-schema compatible library, such as valibot, arktype, and others, while zod continues to be supported out-of-the-box.

    For existing users, your zod schemas 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

17 Aug 20:32
ddc27ea

Choose a tag to compare

Patch Changes

  • #25 711f5f6 Thanks @Katona! - Fix catch-all params can't be mixed with other params

v0.3.2

22 Apr 21:58
6d5d020

Choose a tag to compare

Patch Changes

v0.3.1

30 Mar 15:51
9829e4f

Choose a tag to compare

Patch Changes

  • 4f0e3a5 Thanks @lukemorales! - Fix type definition for useSafeParams when route has both params and searchParams defined

v0.3.0

29 Mar 16:31
0634241

Choose a tag to compare

Minor Changes

  • #15 31d794e Thanks @lukemorales! - Add support for experimental.typedRoutes

    You may now enable experimental.typedRoutes in next.config.js to have a better and safer experience with autocomplete when defining your routes

v0.2.0

27 Mar 00:49
5e7e23e

Choose a tag to compare

Minor Changes

Patch Changes

v0.1.1

04 Feb 03:39
c358d69

Choose a tag to compare

Patch Changes

v0.1.0

02 Feb 19:45
ee40b00

Choose a tag to compare

Minor Changes

  • 8cbcb51 Thanks @lukemorales! - Initial release

    Static 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]
    Ensure experimental.typedRoutes is not enabled in next.config.js

    Declare 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 with URLSearchParams where all values are strings and you might want to convert params to different types. For this reason, this package does not expose types to infer params or searchParams from 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>
      )
    };