Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions web/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

import { TRPCProvider } from '@/lib/trpc/client'
import { SessionProvider } from 'next-auth/react'
import { NuqsAdapter } from 'nuqs/adapters/next/app'
import { Toaster } from '@/components/ui/sonner'

export function Providers({ children }: { children: React.ReactNode }) {
return (
<SessionProvider>
<TRPCProvider>{children}</TRPCProvider>
<Toaster richColors position="top-right" />
</SessionProvider>
<NuqsAdapter>
<SessionProvider>
<TRPCProvider>{children}</TRPCProvider>
<Toaster richColors position="top-right" />
</SessionProvider>
</NuqsAdapter>
)
}
1 change: 1 addition & 0 deletions web/docs/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ The `docs` agent reads this index first to locate the right file.
| Object storage (Cloudflare R2) — presign utility, uploadToR2, useUpload hook | [storage.md](storage.md) | `lib/storage.ts`, `lib/useUpload.ts` |
| Authentication (NextAuth v5) — providers, session, proxy, forms, hooks | [auth.md](auth.md) | `auth.ts`, `proxy.ts`, `features/auth/` |
| DataTable component (TanStack Table v8) — sorting, filtering, pagination, column definitions | [data-table.md](data-table.md) | `components/data-table/DataTable.tsx`, `components/data-table/columns.ts`, `components/data-table/index.ts`, `app/demo/page.tsx`, `app/demo/DemoTable.tsx` |
| URL search-param state (nuqs) — useQueryState, parsers, server cache | [url-state.md](url-state.md) | `app/providers.tsx` |
15 changes: 10 additions & 5 deletions web/docs/trpc.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
topic: trpc
last_verified: 2026-06-24
last_verified: 2026-06-27
sources:
- server/trpc.ts
- server/routers/_app.ts
Expand Down Expand Up @@ -149,18 +149,23 @@ export default async function HealthPage() {

## Provider wiring (`app/providers.tsx` + `app/layout.tsx`)

`app/providers.tsx` is a thin `'use client'` wrapper. `SessionProvider` (from `next-auth/react`) wraps `TRPCProvider` so both session and React Query contexts are available to all Client Components:
`app/providers.tsx` is a thin `'use client'` wrapper. From outermost to innermost: `NuqsAdapter` (URL search-param state), `SessionProvider` (next-auth session), `TRPCProvider` (React Query + tRPC). A `Toaster` is rendered as a sibling of `TRPCProvider` inside `SessionProvider`:

```tsx
'use client'
import { TRPCProvider } from '@/lib/trpc/client'
import { SessionProvider } from 'next-auth/react'
import { NuqsAdapter } from 'nuqs/adapters/next/app'
import { Toaster } from '@/components/ui/sonner'

export function Providers({ children }: { children: React.ReactNode }) {
return (
<SessionProvider>
<TRPCProvider>{children}</TRPCProvider>
</SessionProvider>
<NuqsAdapter>
<SessionProvider>
<TRPCProvider>{children}</TRPCProvider>
<Toaster richColors position="top-right" />
</SessionProvider>
</NuqsAdapter>
)
}
```
Expand Down
108 changes: 108 additions & 0 deletions web/docs/url-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
topic: url-state
last_verified: 2026-06-27
sources:
- app/providers.tsx
---

# URL Search-Param State (nuqs)

[nuqs](https://nuqs.47ng.com) synchronises React state with URL search params. It replaces manual `useSearchParams` + `router.push` wiring and works with Next.js App Router out of the box.

## Adapter

`NuqsAdapter` is already mounted as the outermost provider in `app/providers.tsx`. No extra setup is needed.

## Basic usage

```tsx
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'

export function PageSizeSelector() {
const [pageSize, setPageSize] = useQueryState('pageSize', parseAsInteger.withDefault(20))

return (
<select value={pageSize} onChange={e => setPageSize(Number(e.target.value))}>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
</select>
)
}
```

The URL becomes `?pageSize=50`; removing the param resets to the default.

## Multiple params at once

```tsx
'use client'
import { useQueryStates, parseAsString, parseAsInteger } from 'nuqs'

const [{ q, page }, setSearch] = useQueryStates({
q: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1),
})

// Merge-update (only touches specified keys):
setSearch({ page: 2 })

// Full replace:
setSearch({ q: 'hello', page: 1 })
```

## Built-in parsers

| Parser | URL value | JS value |
|---|---|---|
| `parseAsString` | `?q=hello` | `'hello'` |
| `parseAsInteger` | `?page=2` | `2` |
| `parseAsBoolean` | `?open=true` | `true` |
| `parseAsFloat` | `?price=9.99` | `9.99` |
| `parseAsIsoDateTime` | `?date=2026-06-27T...` | `Date` |
| `parseAsArrayOf(parseAsString)` | `?tags=a&tags=b` | `['a', 'b']` |
| `parseAsJson(zodSchema.parse)` | `?filter=%7B...%7D` | parsed object |

## Server Component access

Server Components receive search params as a prop — no hook needed:

```tsx
// app/search/page.tsx
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; page?: string }>
}) {
const { q = '', page = '1' } = await searchParams
// pass to server action / tRPC caller
}
```

Use `createSearchParamsCache` from nuqs/server to parse and validate in one step:

```tsx
import { createSearchParamsCache, parseAsString, parseAsInteger } from 'nuqs/server'

export const searchParamsCache = createSearchParamsCache({
q: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1),
})

export default async function SearchPage({ searchParams }: { searchParams: Promise<Record<string, string>> }) {
const { q, page } = searchParamsCache.parse(await searchParams)
// ...
}
```

## When to use nuqs vs. plain `useSearchParams`

| Scenario | Use |
|---|---|
| Syncing filter/sort/pagination UI with the URL | `useQueryState` / `useQueryStates` |
| Reading params in a Server Component | `searchParams` prop (or `createSearchParamsCache`) |
| One-off read-only access in a Client Component | `useSearchParams()` (Next.js built-in) |
| Programmatic navigation without state binding | `router.push` / `router.replace` |

Prefer nuqs whenever a Client Component both reads and writes a search param — it handles serialisation, defaults, and shallow routing automatically.
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"next": "16.2.9",
"next-auth": "5.0.0-beta.31",
"next-themes": "^0.4.6",
"nuqs": "^2.8.9",
"radix-ui": "^1.6.0",
"react": "19.2.4",
"react-dom": "19.2.4",
Expand Down
36 changes: 36 additions & 0 deletions web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading