Skip to content

Commit

Permalink
doc: v2 announcement blog post (#687)
Browse files Browse the repository at this point in the history
* doc: Add blog engine from Fumadocs

* doc: Add v2 announcement banners

* doc: Make blog posts look pretty

* doc: Blog post contents

* doc: Add og:images

* doc: Add blog post og:image

* doc: Wording fixes

* doc: Add v2 blog link to playground
  • Loading branch information
franky47 committed Oct 22, 2024
1 parent f198605 commit 9d2a514
Show file tree
Hide file tree
Showing 36 changed files with 404 additions and 111 deletions.
162 changes: 162 additions & 0 deletions packages/docs/content/blog/nuqs-2.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
---
title: nuqs 2
description: Opening up to other React frameworks
author: François Best
date: 2024-10-22
---

[email protected] is available, try it now:

```bash
pnpm add nuqs@latest
```

It's packing exciting features & improvements, including:

- [Support for other React frameworks](#hello-react): Next.js, React SPA, Remix, React Router, and more to come
- A built-in [testing adapter](#testing) to unit-test your components in isolation
- [Bundle size improvements](#bundle-size-improvements)
- Interactive documentation, with [community parsers](/docs/parsers/community)

<hr/>

## Hello, React! 👋 ⚛️ [#hello-react]

nuqs started as a Next.js-only hook, and v2 brings compatibility for other React frameworks:

- Next.js 14 & 15 (app & pages routers)
- React SPA
- Remix
- React Router

No code change is necessary in components that use nuqs hooks,
making them **universal** across all supported frameworks.

The only new requirement is to wrap your React tree with an
[adapter](/docs/adapters) for your framework.

Example for a React SPA with Vite:

```tsx title="src/main.tsx"
// [!code word:NuqsAdapter]
import { NuqsAdapter } from 'nuqs/adapters/react'

createRoot(document.getElementById('root')!).render(
<NuqsAdapter>
<App />
</NuqsAdapter>
)
```

<Callout>
The [adapters documentation](/docs/adapters) has examples for all supported frameworks.
</Callout>

## Testing

One of the major pain points with nuqs v1 was testing components that used its hooks.

Nuqs v2 comes with a built-in [testing adapter](/docs/testing) that mocks URL behaviours,
allowing you to test your components in isolation, outside of any framework runtime.

You can use it with any unit testing framework that renders React components
(I recommend [Vitest](https://vitest.dev) & [Testing Library](https://testing-library.com/)).

```tsx title="counter-button.test.tsx"
// [!code word:NuqsTestingAdapter]
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { NuqsTestingAdapter, type UrlUpdateEvent } from 'nuqs/adapters/testing'
import { describe, expect, it, vi } from 'vitest'
import { CounterButton } from './counter-button'

it('should increment the count when clicked', async () => {
const user = userEvent.setup()
const onUrlUpdate = vi.fn<[UrlUpdateEvent]>()
render(<CounterButton />, {
// 1. Setup the test by passing initial search params / querystring:
wrapper: ({ children }) => (
<NuqsTestingAdapter searchParams="?count=42" onUrlUpdate={onUrlUpdate}>
{children}
</NuqsTestingAdapter>
)
})
// 2. Act
const button = screen.getByRole('button')
await user.click(button)
// 3. Assert changes in the state and in the (mocked) URL
expect(button).toHaveTextContent('count is 43')
expect(onUrlUpdate).toHaveBeenCalledOnce()
expect(onUrlUpdate.mock.calls[0][0].queryString).toBe('?count=43')
expect(onUrlUpdate.mock.calls[0][0].searchParams.get('count')).toBe('43')
expect(onUrlUpdate.mock.calls[0][0].options.history).toBe('push')
})
```

The adapter conforms to the **setup** / **act** / **assert** testing strategy, allowing you
to:

1. Set the initial URL search params
2. Let your test framework perform actions on your component
3. Asserting on how the URL was changed as a result

## Breaking changes & migration

The biggest breaking change is the introduction of [adapters](/docs/adapters).
Another one is related to deprecated APIs.

The `next-usequerystate` package that started this journey is no longer updated.
All updates are now published under the `nuqs` package name.

The minimum version of Next.js supported is now 14.2.0. It is compatible with
Next.js 15, including the async `searchParams{:ts}` page prop in the [server-side cache](/docs/server-side).

There are some important behaviour changes, based on feedback from the community:

- [`clearOnDefault{:ts}`](/docs/options#clear-on-default) is now `true{:ts}` by default
- [`startTransition{:ts}`](/docs/options#transitions) no longer sets `shallow: false{:ts}`
- [`parseAsJson{:ts}`](/docs/parsers/built-in#json) now requires a validation function

<Callout>
Read the complete [migration guide](/docs/migrations/v2) to update your applications.
</Callout>

## Bundle size improvements

By moving to **ESM-only**, and dropping hacks needed to support older versions of Next.js,
the bundle size is now **20% smaller** than v1. It's also **side-effects free** and **tree-shakable**.

## What's next?

The community and I have a lot of ideas for the future of nuqs, including:

- A unified, scalable, type-safe routing experience in all supported React frameworks
- Community-contributed parsers & adapters
- New options: debouncing, global defaults override
- Middleware to migrate old URLs to new ones
- Better Zod integration for type-safe & runtime-safe validation

## Thanks

I want to thank [sponsors](https://github.com/sponsors/franky47),
[contributors](https://github.com/47ng/nuqs/graphs/contributors)
and people who raised issues and discussions on
[GitHub](https://github.com/47ng/nuqs) and [X/Twitter](https://x.com/nuqs47ng).
You are the growing community that drives this project forward,
and I couldn't be happier with the response.

### Sponsors

- [Pontus Abrahamsson](https://x.com/pontusab), founder of [Midday.ai](https://midday.ai)
- [Carl Lindesvard](https://x.com/CarlLindesvard), founder of [OpenPanel](https://openpanel.dev)
- [Robin Wieruch](https://x.com/rwieruch), author of [The Road to Next](https://www.road-to-next.com/)
- [Yoann Fleury](https://x.com/YoannFleuryDev)
- [Sunghyun Cho](https://github.com/anaclumos)
- [Jalol](https://github.com/mirislomovmirjalol)

Thanks to these amazing people, I'm able to dedicate more time to this project and make it better for everyone.
Join them on [GitHub Sponsors](https://github.com/sponsors/franky47)!

### Contributors

Huge thanks to [@andreisocaciu](https://github.com/andreisocaciu), [@tordans](https://github.com/tordans), [@prasannamestha](https://github.com/prasannamestha), [@Talent30](https://github.com/Talent30), [@neefrehman](https://github.com/neefrehman), [@chbg](https://github.com/chbg), [@dopry](https://github.com/dopry), [@weisisheng](https://github.com/weisisheng), [@hugotiger](https://github.com/hugotiger), [@iuriizaporozhets](https://github.com/iuriizaporozhets), [@rikbrown](https://github.com/rikbrown), [@mateogianolio](https://github.com/mateogianolio), [@timheerwagen](https://github.com/timheerwagen), [@psdmsft](https://github.com/psdmsft), and [@psdewar](https://github.com/psdewar) for helping!
28 changes: 26 additions & 2 deletions packages/docs/content/docs/adapters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ wrapping it with a `NuqsAdapter` context provider:
- [Remix](#remix)
- [React Router](#react-router)

## Next.js (app router)
## Next.js

### App router [#nextjs-app-router]

Wrap your `{children}{:ts}` with the `NuqsAdapter{:ts}` component in your root layout file:

```tsx title="src/app/layout.tsx"
// [!code word:NuqsAdapter]
Expand All @@ -34,7 +38,9 @@ export default function RootLayout({
}
```

## Next.js (pages router)
### Pages router [#nextjs-pages-router]

Wrap the `<Component>{:ts}` page outlet with the `NuqsAdapter{:ts}` component in your `_app.tsx` file:

```tsx title="src/pages/_app.tsx"
// [!code word:NuqsAdapter]
Expand All @@ -50,6 +56,18 @@ export default function MyApp({ Component, pageProps }: AppProps) {
}
```

### Unified (router-agnostic) [#nextjs-unified]

If your Next.js app uses **both the app and pages routers** and the adapter needs
to be mounted in either, you can import the unified adapter, at the cost
of a slightly larger bundle size (~100B).

```tsx
import { NuqsAdapter } from 'nuqs/adapters/next'
```

<br/>

The main reason for adapters is to open up nuqs to other React frameworks:

## React SPA
Expand Down Expand Up @@ -107,3 +125,9 @@ export function ReactRouter() {
)
}
```

## Testing

<Callout>
Documentation for the `NuqsTestingAdapter{:ts}` is on the [testing page](/docs/testing).
</Callout>
8 changes: 5 additions & 3 deletions packages/docs/content/docs/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ bun add nuqs

## Which version should I use?

`nuqs` supports the following frameworks and their respective versions:
`nuqs@^2` supports the following frameworks and their respective versions:

- [Next.js](./adapters#nextjs-app-router): 14.2.0 and above (including Next.js 15)
- [Next.js](./adapters#nextjs): 14.2.0 and above (including Next.js 15)
- [React SPA](./adapters#react-spa): 18.3.0 & 19 RC
- [Remix](./adapters#remix): 2 and above
- [React Router](./adapters#react-router): 6 and above

For older versions of Next.js, you may use `nuqs@^1`.
<Callout>
For older versions of Next.js, you may use `nuqs@^1` (documentation in the README).
</Callout>

8 changes: 3 additions & 5 deletions packages/docs/content/docs/migrations/v2.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export default function MyApp({ Component, pageProps }: AppProps) {
#### Unified (router-agnostic)

If your Next.js app uses **both the app and pages routers** and the adapter needs
to be mounted in either, you can use the unified adapter, at the cost
to be mounted in either, you can import the unified adapter, at the cost
of a slightly larger bundle size (~100B).

```tsx
Expand All @@ -93,8 +93,6 @@ import { NuqsAdapter } from 'nuqs/adapters/next'
Albeit not part of a migration from v1, you can now use nuqs in other React
frameworks via their respective [adapters](/docs/adapters).

{/* todo: Add the docs/adapters page */}

However, there's one more adapter that might be of interest to you, and solves
a long-standing issue with testing components using nuqs hooks:

Expand Down Expand Up @@ -188,7 +186,7 @@ const { useQueryState } = await import('nuqs')
Some of the v1 API was marked as deprecated back in September 2023, and has been
removed in `[email protected]`.

### `queryTypes{:ts}` parsers object
### `queryTypes` parsers object

The `queryTypes{:ts}` object has been removed in favor of individual parser exports,
for better tree-shaking.
Expand All @@ -205,7 +203,7 @@ Replace with `parseAsXYZ{:ts}` to match:
+ useQueryState('page', parseAsInteger.withDefault(1))
```

### `subscribeToQueryUpdates{:ts}`
### `subscribeToQueryUpdates`

Next.js 14.1.0 makes `useSearchParams{:ts}` reactive to shallow search params updates,
which makes this internal helper function redundant. See [#425](https://github.com/47ng/nuqs/pull/425) for context.
Expand Down
23 changes: 14 additions & 9 deletions packages/docs/content/docs/server-side.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ title: Server-Side usage
description: Type-safe search params on the server
---

<Callout>
This feature is available for Next.js only.
</Callout>

If you wish to access the searchParams in a deeply nested Server Component
(ie: not in the Page component), you can use `createSearchParamsCache`
(ie: not in the Page component), you can use `createSearchParamsCache{:ts}`
to do so in a type-safe manner.

<Callout type="warn" title="Note">
Expand Down Expand Up @@ -32,15 +36,16 @@ export const searchParamsCache = createSearchParamsCache({

```tsx title="page.tsx"
import { searchParamsCache } from './searchParams'
import { type SearchParams } from 'nuqs/server'

type PageProps = {
searchParams: Promise<SearchParams> // Next.js 15+: async searchParams prop
}

export default function Page({
searchParams
}: {
searchParams: Record<string, string | string[] | undefined>
}) {
export default async function Page({ searchParams }: PageProps) {
// ⚠️ Don't forget to call `parse` here.
// You can access type-safe values from the returned object:
const { q: query } = searchParamsCache.parse(searchParams)
const { q: query } = searchParamsCache.parse(await searchParams)
return (
<div>
<h1>Search Results for {query}</h1>
Expand Down Expand Up @@ -81,8 +86,8 @@ import { coordinatesCache } from './searchParams'
import { Server } from './server'
import { Client } from './client'

export default function Page({ searchParams }) {
coordinatesCache.parse(searchParams)
export default async function Page({ searchParams }) {
coordinatesCache.parse(await searchParams)
return (
<>
<Server />
Expand Down
Loading

0 comments on commit 9d2a514

Please sign in to comment.