Back

Beyond useState: State Management in Next.js using URL Parameters

Beyond useState: State Management in Next.js using URL Parameters

With frontend frameworks like React.js and Next.js, useState is a popular way to manage application states. However, as apps grow, developers seek more advanced ways to manage states for better user experience, SEO, and optimization, which leads to other options, such as URL parameters. This article explores using URL parameters to manage the state in a Next.js project.

The useState hook is still the most commonly used React hook for managing state in functional components. However, it has limitations, particularly as the application grows and more components require data. This often leads to the practice of passing data from component to component, known as props drilling or using state management libraries. Below are a few limitations:

  • Local component scope: useState is designed to manage the state of the defined component. If you need to share a state across multiple components or manage a global state, consider using the useContext hook from React or a state management library like Redux-toolkit.
  • Not optimized for SEO: SEO Can be impacted as URL parameters don’t reflect state changes made with useState.
  • Consideration for User Experience: In an e-commerce app, not leveraging URL parameters can result in a poor user experience, as users cannot share their preferences with others.
  • Prop drilling: If you use useState to manage the global state, you must pass the state and the setter function as props to every component that needs them. This can make your code hard to follow, especially if you have deeply nested components.

To overcome these limitations, you may want to use other state management options, such as the useReducer hook, the useContext hook, or third-party libraries like Redux, and MobX.

Why Use URL Parameters over useState?

To understand why using URL parameters is better than useState, it’s important to understand what URL parameters are.

URL parameters are query strings added at the URL’s end using a question mark (?). These parameters are used to enhance the functionality of a webpage with features like search and pagination, increase page SEO, and, most of the time, track marketing campaigns. A URL can have multiple parameters separated using an ampersand (&). Example of a URL with parameters. a URL with parameters

So why then use URL parameter over useState?

URL parameters can be a better choice than useState in certain scenarios, especially when managing application state in a more versatile and shareable way. There are several reasons why you might choose URL parameters over useState:

  1. Webpage bookmarking: URL parameters allow you to encode state information directly into the URL. This makes it easy for users to bookmark and share a specific page with others.
  2. Improved state management: In a webpage with search functionality, the search term can be saved for later, even if the user refreshes the browser. This improves the user experience and encourages them to stay on your website.
  3. Simplified component logic: URL parameters can simplify the logic within individual components. Instead of using useState to handle complex search functionality, you can rely on the URL query parameters.

While useState is powerful for managing local component states, combining it with URL parameters offers a more enhanced approach to state management.

Understanding URL parameter query Patterns

URL parameters consist of two parts, namely a key and value pair. The key serves as a placeholder to identify the parameter name, while the value represents the value that binds to the key. An equal sign separates these two parts (=). A URL can have multiple parameters, and each parameter (key and value) is separated by an ampersand (&). For example: https://www.example.com/search?q=mens+t-shirt&size=3xl&color=white&sort=asc This is a URL that leads to a search route. The search parameter begins with q. To separate multiple search queries, the ampersand (&) is used. In this case, the first parameter key is q, the second is size, the third is color, and the last is sort. These parameters together display what the user wants, including sorting the page.

Common URL Parameter Use Cases

Websites commonly use URL parameters to manage advanced states, such as marketing campaigns and page SEO. There are numerous benefits to using URL parameters.

URL parameters are a powerful tool for improving webpage functionality and user experience.

Global State with URL - Pros and Cons

When it comes to managing the state of web applications, using URLs can be beneficial in various ways. It can enhance the user experience, enable tracking of marketing campaigns, and improve page SEO. However, if not used correctly, it can also lead to issues on the webpage. Here are some pros and cons to consider.

Pros

  • Bookmarkable and Shareable URLs: Users can bookmark specific URL states of the application or share them with others, enhancing usability and collaboration.
  • Deep Linking: Developers can use URL parameters to create dynamic pages that match query strings, improving deep linking of application states.
  • Server-Side Rendering (SSR) Compatibility: Using Next.js for projects requiring server-side rendering is ideal, as URL parameters may transmit state data between the server and client.

Cons

  • Security concerns: Sensitive information stored in URL parameters may pose significant security risks, as they can be visible to users and potentially tampered with.
  • Duplicate content: Misuse of URL parameters can lead to multiple confusing URLs and potentially slow page ranking for SEO engines.
  • Complex URL Structures: Complicated query parameters often lead to long, difficult-to-read URLs, discouraging users from clicking and trusting your link, thereby reducing page visits.

Security Considerations

When using URL query parameters, keeping security in mind is crucial. Avoid storing sensitive information, like credit card details, in the parameters. Additionally, don’t display form fields on the URL before sending them to the web server.

Instead, useState manages user preferences like a dark or light theme. Avoid storing item prices or discount codes on the URL, as they can be tampered with.

However, you can store data fetching, page number pagination, tabs, and modal states on the URL to enhance user experience.

How to implement URL Parameters in next.js

Implementing URL query parameters in a Next.js project may seem challenging, but it is quite simple. You will need to have some understanding of the Next.js routing system and how to create a Next.js project. To learn more, check out these two articles. OpenReplay’s article on Routing and Getting started with Next.js

Setting up a project

In this tutorial, I will demonstrate how to use search and sort query parameters with JSONPlaceholder API. You can either follow my example or integrate it into your existing project.

NOTE: As of the time of writing this article, Next.js version 14 has changed the import for the client component’s useRouter to next/navigation instead of next/router. If you’re using an older version from 12 and below, you can still use import from next/router, but you may find the section slightly different. However, versions 13 and above should not have any issues.

Creating folders

There are various methods to generate query parameters. In this tutorial, I will use a specific route named /search to manage the queries. You can use a different approach if you prefer. Keeping this in mind, let’s look at your application folder’s structure.

folder structure

In the image above, the component folder will contain all the reusable components like SearchInput and DisplayData. Next, the api/users folder will handle the data fetching from the placeholder API. Finally, the search folder represents the actual /search route, which will navigate users when searching or sorting on the homepage.

How to fetch data from API in next.js

To retrieve data from the JSONplaceholder API, we will use the routing system in next.js 14. This system suggests putting all API requests into the api folder. To follow this convention, you can create a new file called route.js inside the api/users folder and copy-paste the code snippet below.

import { NextResponse } from "next/server";

async function fetchUsers() {
  const baseUrl = "https://jsonplaceholder.typicode.com/users";
  const response = await fetch(baseUrl);
  const data = await response.json();

  return data;
}

export async function GET(request) {
  const posts = await fetchUsers();

  return NextResponse.json(posts);
}

We import the nextResponse function from Next.js in the following code block. This function enables us to make our return data from JSONPlaceholder API available to the public endpoint.

Then, we define a function called fetchUsers which fetches user data from our API. Once we have the data, we expose it to the public endpoint.

Clear the clutter

To use the API route we created, open the page.js file in the App folder and remove all the boilerplate code. Copy and paste the below code.

"use client";
import { Suspense, useEffect, useState } from "react";

export default function Home() {
  const [data, setData] = useState([]);
  const fetchPosts = async () => {
    const res = await fetch("/api/users");
    const data = await res.json();
    setData(data);
  };

  useEffect(() => {
    fetchPosts();
  }, []);
  return (
    <div className="m-12">
      <SearchSortInput />
      <Suspense fallback={<div>Loading...</div>}>
        <DisplayData data={data} />
      </Suspense>
    </div>
  );
}

As previously discussed, we will display the data on the homepage. If a user searches for that data, We will append their search query to the URL and navigate to the /search route, similar to how Netflix manages its search functionality. To accomplish this, we will create two additional components to handle these functionalities.

Creating components

Create two components. First, a search input component that handles appending the search and sort queries to the URL.

import { useRouter, useSearchParams } from "next/navigation";

To begin with, we import our query hooks from next/navigation. The useRouter hook enables us to navigate to any route within our client application. On the other hand, the useSearchParams hook allows us to manipulate the queries from the URL such as get, set, and delete methods.

const SearchSortInput = () => {
  const router = useRouter();
  const searchParams = useSearchParams();
  const query = searchParams?.get("q");
  const sort = searchParams?.get("sort");

  const newParams = URLSearchParams(searchParams.toString());
};

Next, we initialize our hooks, and using the search parameters, we retrieve existing queries from the URL. This allows us to persist any query in our input fields.

return (
  <div className="flex items-center space-x-4 mb-4">
    <button
      onClick={() => router.push("/")}
      className="border border-gray-300 p-2 rounded text-black border-black"
    >
      Home
    </button>

    <form
      className="
        flex items-center space-x-4 mb-4 mx-auto
      "
    >
      <input
        type="text"
        placeholder="Search..."
        name="search"
        key={query || ""}
        defaultValue={query || ""}
        className="border border-gray-300 p-2 rounded text-black border-black"
      />
      <button
        type="submit"
        className="border border-gray-300 p-2 rounded text-black border-black"
      >
        Search
      </button>
      <div className="flex gap-2 items-center">
        <p>Sort by:</p>

        <select
          defaultValue={sort || "default"}
          name="sort"
          onChange={(e) => {
            newParams.set("sort", e.target.value);
            router.push(`/search?${newParams.toString()}`);
          }}
          className="border border-gray-300 p-2 rounded"
        >
          <option value="default">Default</option>
          <option value="title">Name</option>
          <option value="asc">Ascending</option>
          <option value="desc">Descending</option>
          <option value="a-z">A to Z</option>
        </select>
      </div>
    </form>
  </div>
);

In this section, we are creating input fields for the user to type search queries. Instead of using useState to handle the updates of the inputs, we set the defaultValue of the inputs to our existing queries. This way, even if the user navigates away from the page or refreshes it, their queries will still be there. This is one of the benefits of using URL query parameters. To style our input fields, we are using TailwindCSS.

 const handleSubmit = (event) => {
  event.preventDefault();
  const val = event.target;
  const search = val.search;
  const sortBy = val.sort;

  if (search.value) {
    newParams.set("q", search.value);
  } else {
    newParams.delete("q");
  }
  if (sortBy.value) {
    newParams.set("sort", sortBy.value);
  } else {
    newParams.delete("sort");
  }
  router.push(`/search?${newParams.toString()}`);
};

This function is responsible for processing the query logic. We are not using useState to manage our inputs, so we retrieve the values from the form. If the search input has a value, we create a new query with the key q and the value being whatever the user has typed in. If the search input is empty, we remove the query. The same process is repeated for sorting. Finally, we navigate to the /search route and add the queries to the URL.

return (
  <div className="flex items-center space-x-4 mb-4">
    // other codes here...
    <form onSubmit={handleSubmit}>
    // inputs here...
    </form>
  </div>
);

export default SearchSortInput;

To complete the logic for this component, we hook up our handleSubmit function to our form and export it.

Creating the data display component

This data display component will handle the filtering of our data from the JSONPlaceholder API` using the user queries.

"use client";

export default function DisplayData({ data, q, sort }) {}

To begin with, we create a function that accepts data, q, and sort parameters. We include use client at the top to indicate that this is a Next.js client component.

const filteredData = () => {
  let newData = [...data];

  if (q) {
    newData = newData.filter(
      (item) =>
        item.name.toLowerCase().includes(q.toLowerCase()) ||
        item.username.toLowerCase().includes(q.toLowerCase()),
    );
  }

  if (sort) {
    newData.sort((a, b) => {
      if (sort === "name") {
        return a.name.localeCompare(b.name);
      } else if (sort === "a-z") {
        return b.username.localeCompare(a.username);
      } else if (sort === "asc") {
        return a.id - b.id;
      } else if (sort === "desc") {
        return b.id - a.id;
      } else {
        return 0;
      }
    });
  }

  return newData;
};

We then create a filteredData function that utilizes JavaScript’s built-in filter and sort methods to search and sort the data. If there are no search or sort queries, we simply return the complete data.

return (
  <div className="flex flex-col items-center">
    <h1
      className="
        text-4xl font-semibold text-center mb-4 mt-8 mx-auto 
      "
    >
      My Feed
    </h1>
    <ul className="grid grid-cols-4 mx-auto max-w-[1260px] gap-10"></ul>
    {filteredData().map((item) => (
      <ul
        key={item.id}
        className="flex border border-gray-300 p-4 rounded w-[600px] mb-4 gap-4"
      >
        <h3 className="text-lg font-semibold mb-2">{item.name}</h3>
        <p className="text-gray-500">Username: {item.username}</p>
        <p className="text-gray-500">Email: {item.email}</p>
      </ul>
    ))}
  </div>
);

Finally, we map through the filtered data and render it.

Creating the search page

This is the page where we display the search results based on user queries. We use the DisplayData component that we created earlier. Open the page.js file inside the search folder and paste the code snippet below.

"use client";
import { useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import DisplayData from "../_components/DisplayData";
import SearchSortInput from "../_components/SearchInput";

export default function Search() {
  const searchParams = useSearchParams();
  const q = searchParams.get("q");
  const sort = searchParams.get("sort");
  const [data, setData] = useState([]);

  useEffect(() => {
    const fetchData = async () => {
      const searchParams = new URLSearchParams();

      if (q) {
        searchParams.append("q", q);
      }

      if (sort) {
        searchParams.append("sort", sort);
      }
      const response = await fetch(`/api/users`);
      const data = await response.json();

      setData(data);
    };

    fetchData();
  }, [q, sort]);

  return (
    <div className="m-12">
      <SearchSortInput />
      {q && (
        <h3 className="text-2xl font-bold mb-4">Search results for: {q}</h3>
      )}
      {sort && <p className="text-[14px] mb-4">Sorted by: {sort}</p>}
      <Suspense fallback={<div>Loading...</div>} key={q}>
        <DisplayData data={data} sort={sort} q={q} />
      </Suspense>
    </div>
  );
}

In the following code snippet, we utilize the searchParams function to extract user queries from the URL. After that, we use the useEffect function to ensure that we fetch our data from the API route only when the page has fully loaded. To display the search results, we import the DisplayData component and pass it in our props. We use the Suspense function from React to display a loading message while our data is still loading.

Finally, to wrap up our URL query implementation in Next.js. Open the page.js file inside the App folder and paste the code snippet below.

"use client";
import { Suspense, useEffect, useState } from "react";
import DisplayData from "./_components/DisplayData";
import SearchSortInput from "./_components/SearchInput";

export default function Home() {
  const [data, setData] = useState([]);
  const fetchPosts = async () => {
    const res = await fetch("/api/users");
    const data = await res.json();
    setData(data);
  };

  useEffect(() => {
    fetchPosts();
  }, []);
  return (
    <div className="m-12">
      <SearchSortInput />
      <Suspense fallback={<div>Loading...</div>}>
        <DisplayData data={data} />
      </Suspense>
    </div>
  );
}

In this file, our main task is to retrieve data from our API route. As this is the homepage, we display our search inputs and the DisplayData component. We are only rendering the components here, while the search and sort logic is implemented on the /search page, which is dedicated to it.

Testing our application

To test our application, open the terminal in Vscode or your terminal of choice, then start the application by running:

npm run dev

homepage of query string implementation

The above image describes what we have just implemented. From this home page, when a user searches or sorts the data, we navigate them to the /search page and perform the filtering.

showing results for search page using URL query parameters

In this search page, queries from the homepage are automatically used to perform the searching or sorting. Additionally, the user can perform new searches or sorts in this search page and instantly see the results without refreshing the page.

Handling edge cases in query parameters

When using query parameters in your application, it is always advisable to handle edge cases, including validating the incoming query parameters to make sure they are safe. Here are some edge cases to consider:

  • Undefined or Null values: It’s important to always provide a fallback value for query parameters that are null or undefined. This ensures that your application does not throw errors.
  • Unexpected values: In order to validate a user in your application using authentication tokens, it is important to sanitize URL query parameters to ensure they are valid.
  • Type Checking: Make sure to validate query parameters against expected types and convert or parse values if necessary. For example, when managing page pagination, always check and parse incoming queries into numbers.
  • Security Consideration: Use regular expressions to remove any special characters in query parameters. This ensures users don’t manipulate the query parameters in your application.

By addressing these edge cases, you can enhance the security of your application when working with query parameters in URLs.

Conclusion

URL query parameters are a powerful tool that can significantly improve website performance and enhance user experience. It’s great to see that many developers are now using query parameters in their codebase while building real-world applications. Big companies like Amazon and Netflix web apps already leverage the query parameters tool.

In this article, we have learned about the importance of URL query parameters and how to implement them in a Next.js project. That’s all for now. Thank you for reading until the end.

You can get the full code here, and happy coding!

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay