Skip to content

Commit

Permalink
chore: create changeset
Browse files Browse the repository at this point in the history
  • Loading branch information
lukemorales committed Feb 2, 2024
1 parent 107729e commit 8cbcb51
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 1 deletion.
167 changes: 167 additions & 0 deletions .changeset/flat-goats-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
---
"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<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.
```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 (
<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
```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 (
<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:

```ts
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>
)
};
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ jobs:
if: steps.bun-cache.outputs.cache-hit != 'true'
run: bun install --frozen-lockfile

- name: ♼ Change package.json name
- name: 🏷️ Overwrite package name with user scope
uses: sergeysova/jq-action@v2
with:
cmd: echo "$( jq '.name="@${{ github.repository }}"' package.json )" > package.json
Expand Down

0 comments on commit 8cbcb51

Please sign in to comment.