Binding Query Params with Next.js
Written by Adnan Chowdhury on September 12, 2022

Yesterday, I was building an web app for someone. Long story short, the owner wanted all of the user input data on a page to be stored in the URL.

That way, if a user wanted to copy and paste the URL and send it to another person, that person would load the page with all of the previous person's data populated in the respective inputs.

This is a standard feature that you will see all the time on the web. For example, try searching for something on Amazon and apply a few filters to the search. Then, copy and paste the URL into a new browser tab. You'll find that your search query and customized filters have been preserved as they were in the original browser tab. It's because Amazon's web app does the job of encoding that important data in the URL.

And that's what we'll explain how to do here, using Next.js!

How can I accomplish this with Next.js?

Next has a useful router object that you can access via the useRouter hook that allows you to access and manipulate state relating to the web browser's address bar. This includes reading the current path, or accessing query params that may be appended to the URL.

import { useRouter } from "next/router";

export default function CoolComponent() {
  const router = useRouter();
  ...

You can use router to read query params from the URL. For example, let's say we have a URL with the query param code that is set to an arbitrary value.

http://localhost:3000/?code=1234atc

You can access this value in the following way:

import { useRouter } from "next/router";

export default function CoolComponent() {
  const router = useRouter();
  const { code } = router.query;
  // code === '1234atc'
  ...

This is very similar to how you access dyanamic route segments with Next.js, however we aren't dealing with Next.js route segments. We are dealing with query parameters that are part of the URI. This is a very old convention that was defined with HTTP 1.1 itself and is used across the web, everywhere.

Setting query params

When storing data in the URL, you want to make sure you update the URL without adding entries to the history stack of the browser. If you're not careful, every value will get stored as a separate 'page' that has been accssed in the browser's back button. Imagine how many entries will get added if you type in a sentence, for an example (an entry for every keystroke).

To avoid this, you'll want to update your URL by utlizing router.replace. What this does is update the URL without adding a new entry in the history stack.

import { useEffect, useState } from "react";
import { useRouter } from "next/router";

export default function CoolComponent() {
  const router = useRouter();
  const [data, setData] = useState('');
  const { code } = router.query;

  useEffect(() => {
    router.replace(`?code=${encodeURIComponent(data)}`);
  }), [data]);
  ...

In the above example, our useEffect hook will be invoked everytime data changes. It will effectively update the URL in the address bar with the latest value of data.

Reading query params

We'll also want to read query params from the URL when first loading the page. This will allow our app to prepopulate the page with any data that is encoded in the URL.

Reading this value requires the use of useEffect and a helpful utility on the router object called isReady:

import { useEffect, useState } from "react";
import { useRouter } from "next/router";

export default function CoolComponent() {
  const router = useRouter();
  const [data, setData] = useState('');
  const { code } = router.query;

  useEffect(() => {
    router.replace(`?code=${encodeURIComponent(data)}`);
  }), [data]);

  useEffect(() => {
    const code = router.query.code ?? '';

    if (router.isReady && code) {
      setData(code);
    }
  }), [router.isReady]);
  ...

We use router.isReady because on initial render, the router object may not yet be up-to-date with the client-side state of things. More on this here. Without checking it, router.query.code will probably be empty (erroneously).

Working example

Here is a fully complete example encapsulated into a single React component, meant to be inside of a Next.js app:

import { useEffect, useState } from "react";
import { useRouter } from "next/router";

export default function CoolComponent() {
  const router = useRouter();
  const [data, setData] = useState("");

  const { code } = router.query;

  useEffect(() => {
    router.replace(`?code=${encodeURIComponent(data)}`);
  }, [data]);

  useEffect(() => {
    const code = (router.query.code as string) ?? "";

    if (router.isReady && code) {
      setData(code);
    }
  }, [router.isReady]);

  return (
    <>
      <label>
        Enter code here:
        <input
          type="text"
          value={data}
          onChange={(e) => setData(e.currentTarget.value)}
        />
      </label>
    </>
  );
}
Conclusion

This article has gone over how to bind query params in your address bar with state variables inside of your Next.js app. You can leverage this in many ways to create a powerful experience for users.

One important caveat that is unique to Next.js is to use router.isReady for any operations that read from the address bar on page load.

Since there is a server-side / client-side distinction with all Next apps, this is something you'll need to remember to avoid the pitfall of attempting to load client-side data on the server-side.

Feel free to reach out with any comments or questions. Find me on Twitter or send an e-mail to adnan at redpine dot software.

portrait of the author
Adnan Chowdhury
© 2022 Adnan Chowdhury