Playing with Search Params using nuqs in Next.js App Router

Playing with Search Params using nuqs in Next.js App Router

M. Zakyuddin Munziri

M. Zakyuddin Munziri

@zakiego

Originally written in Bahasa Indonesia.

Abstract

nuqs is a search params state manager library for Next.js.

The default behavior of nuqs is client-first, meaning when updating search params, the web page won't re-render. This is great if the search params value only needs to be read on the client side.

However, if the value needs to be read by the server, changes won't be detected. To trigger a re-render so the server can read the changes, you need to add the { shallow: false } option.

This differs from next-query-params which adopts a server-first approach, meaning every value change is immediately read by the server.

Experience the difference between client-side and server-side search params in nuqs at nuqs-playground.vercel.app

For next-query-params, you can try it at seach-params-playground.vercel.app

Introduction

This article is a continuation of Creating a Search Component in Next.js App Router. Previously, we used the next-query-params library to manage search params. This time, we'll play with nuqs.

Actually, I had tried nuqs before. But due to limited knowledge, I didn't continue with it.

Thanks to @alfonsusac for triggering me to read the nuqs documentation more deeply.

Overview

nuqs defines itself as "Type-safe search params state manager for Next.js".

Let's begin. Of course, the framework used in this article is Next.js.

First, install nuqs.

pnpm add nuqs

Create a component file containing an <input/>, then forward the onChange value to search params using useQueryState.

// src/app/client.tsx

'use client'

import { useQueryState } from 'nuqs'

export function Demo() {
  const [name, setName] = useQueryState("name");
  return (
    <>
      <input value={name || ""} onChange={(e) => setName(e.target.value)} />
      <p>Hello, {name}!</p>
    </>
  );
}

Why do we need to write "use client" at the very top?

Because since the introduction of the App Router paradigm in Next.js, every component is a server component by default. However, in this case, we need the onChange from <input/> which is a client component. Therefore, we need to write "use client" at the top line to tell React that this is a client component (Client Components).

Finally, import the <Demo/> component we created into /src/app/hello/page.tsx.

The result is that every time you enter a value in <input/>, the search params will be updated immediately.

For example, example.com/hello will change to example.com/hello?name=zaki.

For more examples, check out nuqs.47ng.com/playground.

Difference between nuqs and next-query-params

Previously, I used nuqs in this tweet. The most noticeable difference between nuqs and next-query-params is:

  • nuqs runs on client-first 
  • while next-query-params is server-first

What's the difference?

You can directly experience the difference between client-first (nuqs-playground.vercel.app/nuqs-client) and server-first (nuqs-playground.vercel.app/nuqs-server)

With nuqs being client-first, we cannot access search params from the server side.

For example, we visit the page example.com/hello.

Then, when typing a name in the input, the URL changes to example.com/hello?name=zaki.

Although the URL changes, because nuqs is client-first, the value of name we get from search params will remain empty, if using server side.

nuqs by default doesn't trigger a page update to the server. So even though the URL in our browser changes to example.com/hello?name=zaki, the server still considers us to be on the example.com/hello page only.

interface Props {
  searchParams: {
    name?: string;
  };
}

export default async function Page(props: Props) {
  const { searchParams } = props;
  const { name } = searchParams;

  return (
    <div>
      value from server: {name}
    </div>
  );
}

However, it's different in client components, like in the <Demo/> earlier.

Let me place the code once more.

// src/app/client.tsx

'use client'

import { useQueryState } from 'nuqs'

export function Demo() {
  const [name, setName] = useQueryState("name");
  return (
    <>
      <input value={name || ""} onChange={(e) => setName(e.target.value)} />
      <p>Hello, {name}!</p>
    </>
  );
}

In that code, the value of name will be read immediately, even though we don't update the page to the server.

This is the opposite of next-query-params, where every time you update the value in <input/>, it will trigger a page update.

As a result, when the URL updates from example.com/hello to example.com/hello?name=zaki, the server immediately recognizes that we're already at the URL example.com/hello?name=zaki.

nuqs but Server Side

After reading more carefully, although nuqs is client-first, we can still run a re-render by adding { shallow: false }.

Docs nuqs.47ng.com/docs/options

useQueryState("foo", { shallow: false });

With the above setting, we can get search params through the server side.

You can try it here nuqs-playground.vercel.app/nuqs-server.

Bonus

Here are some replies from the nuqs author directly:

Also, it turns out nuqs is used by vercel.com too

Conclusion

nuqs has a better DX (developer experience) compared to next-query-params, because you don't need to add code to the layout feature.

However, because nuqs is client-first, the search params value changes cannot be obtained directly on the server side, unless using { shallow: false }. This differs from next-query-params which is server-first from the start, so value changes can be read by the server immediately.

In the end, it all comes back to the developer. The questions are:

  1. Do you fetch on the server-side or client side?
  2. Next, does the search params value need to be readable by the server? Because in some cases, search params don't have to be immediately readable by the server

That's all, thank you.


Finished writing on Thursday, April 11, 2024 at 13:39, the second day of Eid.

More Articles

I Stopped Digging Through Logs

I Stopped Digging Through Logs

Debugging changed when I stopped reading logs manually and started using AI agents to correlate errors across observability data - faster root cause, fewer dead ends.

Speed Was Never the Hard Part in CI CD

Speed Was Never the Hard Part in CI CD

Fast pipelines don't eliminate shipping fear. Confidence comes from safe rollbacks, feature flags, and systems that behave predictably when things go wrong.