diff --git a/.changeset/flat-goats-pull.md b/.changeset/flat-goats-pull.md deleted file mode 100644 index c8aabf8..0000000 --- a/.changeset/flat-goats-pull.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -"next-safe-navigation": minor ---- -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](https://nextjs.org) with Zod schemas.** - -> [!WARNING] -> Ensure `experimental.typedRoutes` is not enabled in `next.config.js` - -### Declare your application routes and parameters in a single place -```ts -// 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: -> ```ts -> interface CustomersPageProps { -> // ❌ Do not declare your params | searchParam types -> searchParams?: ReturnType -> } ->``` -> 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. - -```ts -// 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 ( -
- - - -
- ) -}; - -/* --------------------------------- */ - -// 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 ( -
- -
- ) -}; -``` - -### Runtime validation for Client Components -```ts -// 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 ( -
- - - -
- ) -}; - -/* --------------------------------- */ - -// 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 ( -
- -
- ) -}; -``` - -Use throughout your codebase as the single source for navigating between routes: - -```ts -import { routes } from "@/shared/navigation"; - -export function Header() { - return ( - - ) -}; - -export function CustomerInvoices({ invoices }) { - return ( - - ) -}; diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..49ab80b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,175 @@ +# next-safe-navigation + +## 0.1.0 + +### Minor Changes + +- [`8cbcb51`](https://github.com/lukemorales/next-safe-navigation/commit/8cbcb5150724add6351b445db557eb63d941ce63) Thanks [@lukemorales](https://github.com/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](https://nextjs.org) with Zod schemas.** + + > [!WARNING] + > Ensure `experimental.typedRoutes` is not enabled in `next.config.js` + + ### Declare your application routes and parameters in a single place + + ```ts + // 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: + > + > ```ts + > interface CustomersPageProps { + > // ❌ Do not declare your params | searchParam types + > searchParams?: ReturnType; + > } + > ``` + > + > 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. + + ```ts + // 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 ( +
+ + + +
+ ) + }; + + /* --------------------------------- */ + + // 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 ( +
+ +
+ ) + }; + ``` + + ### Runtime validation for Client Components + + ```ts + // 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 ( +
+ + + +
+ ) + }; + + /* --------------------------------- */ + + // 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 ( +
+ +
+ ) + }; + ``` + + Use throughout your codebase as the single source for navigating between routes: + + ```ts + import { routes } from "@/shared/navigation"; + + export function Header() { + return ( + + ) + }; + + export function CustomerInvoices({ invoices }) { + return ( + + ) + }; + ``` diff --git a/package.json b/package.json index 1bd60e0..a49b8c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "next-safe-navigation", - "version": "0.0.2", + "version": "0.1.0", "author": "Luke Morales ", "description": "Type-safe navigation for NextJS App router", "license": "MIT",