OWolf

2024-09-09 web, development, javascript

URL Based State Management in Next.js

By O. Wolfson

A common web development practice is where state is actually stored in the browser's URL as query parameters. This might be referred to as URL State Management. For example, this is a common practice for pagination, where the page number and number of items per page are stored in the URL.

Example: http://myapp.com/blog?limit=4&page=2

Base url: http://myapp.com/blog Query parameters: ?limit=4&page=2

This approach has its roots in the early days of the web, where interactive elements on websites were limited and server-side rendering was prevalent. Storing state in the URL made it easy to maintain state across page reloads and server requests. Historically, as the web evolved from static pages to more dynamic and interactive experiences, developers sought ways to preserve and share the state of web applications. Utilizing the URL's query parameters became a popular method for achieving this, especially for features like pagination, where the state needs to be consistent across client and server interactions.

In this article, we'll explore the advantages of storing state in the URL and how it can be implemented in a Next.js application.

URL State Management

In the URL http://myapp.com/blog?limit=4&page=2, the main components are the base URL (http://myapp.com/blog) and the query parameters (?limit=4&page=2). This URL is a typical example of URL State Management in a web application, specifically in the context of a blog page with pagination. Here's a breakdown of what's going on:

  1. Base URL (http://myapp.com/blog):

    • This part of the URL specifies the web address where the blog is located. It directs the browser to the 'blog' section of the website hosted at myapp.com.
  2. Query Parameters (?limit=4&page=2):

    • The ? symbol marks the beginning of the query parameters in the URL.
    • limit=4: This parameter indicates the number of blog posts to display on a single page. In this case, it's set to show 4 posts per page. This is a part of the pagination functionality, controlling how much content is rendered at one time for the user.
    • page=2: This parameter specifies the current page number in the pagination sequence. It tells the web application to display the second page of the blog posts.

Advantages of Storing State in URL

  1. Easy Bookmarking and Link Sharing: By storing state information like page number and post limit in the URL, users can bookmark, share, or return to a specific state of the web page. For instance, sharing a link to the third page of a blog roll is as simple as copying and sharing the URL.

  2. Improved Search Engine Optimization (SEO): Search engines can crawl and index multiple pages of content more efficiently when the distinct pages are represented by unique URLs. This helps in better organizing and presenting content in search results.

  3. State Persistence: Storing the state in the URL ensures that the state is preserved even when the page is reloaded or accessed from different browsers or devices. This is particularly important for server-side rendered applications.

  4. Enhanced User Experience: Users can easily understand and manipulate the URL to navigate through the content. For example, changing the page number directly in the URL provides a quick way to jump to a specific page.

  5. No Need for Additional State Management: Using the URL to store state reduces the need for additional client-side state management tools or cookies, simplifying the application architecture.

In the code snippet below, from a Nest.js app, the page, currentPage, and limit of posts per page, postsPerPage, values are derived from the URL's query parameters (searchParams). This design choice reflects the advantages described above, making the blog roll both user-friendly and optimized for web standards.

javascript
// Importing necessary components and functions
import Link from "next/link";
import { getPosts } from "@/lib/posts.mjs";

// Defining the BlogPost interface for TypeScript type checking
interface BlogPost {
  slug: string;
  type: string;
  date: string;
  title: string;
  description: string;
  image: string;
  author: string;
  tags: string[];
  formattedDate?: string; // Optional property
}

// Async function component 'Blog'
const Blog = async ({
  searchParams,
}: {
  searchParams: { [key: string]: string | string[] | undefined },
}) => {
  // Parsing and setting the current page from search parameters, defaults to 1
  const currentPage =
    typeof searchParams.page === "string" ? Number(searchParams.page) : 1;
  // Parsing and setting posts per page, defaults to 5
  const postsPerPage =
    typeof searchParams.limit === "string" ? Number(searchParams.limit) : 5;

  // Retrieving posts with filters (type, page, limit) and calculating total pages
  const { posts: blogs, totalPosts } = getPosts(
    "blog",
    postsPerPage,
    currentPage
  );
  const totalPages = Math.ceil(totalPosts / postsPerPage);

  // Determining if the 'Previous' and 'Next' pagination buttons should be disabled
  const isPreviousDisabled = currentPage <= 1;
  const isNextDisabled = currentPage >= totalPages;
  const disabledLinkStyle = "opacity-50 cursor-not-allowed";

  // Render block
  return (
    <div>
      {/* Mapping over the blogs array to display each post */}
      <ul className="flex flex-col gap-4">
        {blogs.map((blog: BlogPost) => (
          <li key={blog.slug} className="border px-3 py-2 rounded-xl">
            <Link href={`/blog/${blog.slug}`}>
              <h3 className="text-2xl font-bold">{blog.title}</h3>
              <div className="text-sm">{blog.formattedDate}</div>
              <div>{blog.description}</div>
            </Link>
          </li>
        ))}
      </ul>
      {/* Pagination Controls */}
      <div className="flex gap-2 py-6 items-center px-2">
        {currentPage === 1 ? (
          <span className={`${disabledLinkStyle}`}>{`<<`}</span>
        ) : (
          <span>
            <Link href={`/blog?limit=${postsPerPage}&page=${1}`}>{`<<`}</Link>
          </span>
        )}
        {isPreviousDisabled ? (
          <span className={`${disabledLinkStyle}`}>Previous</span>
        ) : (
          <Link
            className={``}
            href={`/blog?limit=${postsPerPage}&page=${currentPage - 1}`}
          >
            Previous
          </Link>
        )}

        <span>- {`Page ${currentPage} of ${totalPages}`} -</span>

        {isNextDisabled ? (
          <span className={`${disabledLinkStyle}`}>Next</span>
        ) : (
          <Link
            className={``}
            href={`/blog?limit=${postsPerPage}&page=${currentPage + 1}`}
          >
            Next
          </Link>
        )}
        {currentPage === totalPages ? (
          <span className={`${disabledLinkStyle}`}>{`>>`}</span>
        ) : (
          <span>
            <Link
              href={`/blog?limit=${postsPerPage}&page=${totalPages}`}
            >{`>>`}</Link>
          </span>
        )}
      </div>
    </div>
  );
};
export default Blog;

Explanation

  • Interface BlogPost: Defines the structure for a blog post object for TypeScript type checking.
  • Component Blog: This is an asynchronous functional component that accepts searchParams as props. These parameters are used to control pagination.
  • Pagination Logic: It calculates the current page and posts per page based on the URL's query parameters. It then fetches the relevant blog posts and calculates the total number of pages.
  • Rendering Blog Posts: The component maps over the array of blog posts, rendering each post's details in a list item.
  • Pagination Controls: It provides buttons for navigating through pages, with conditions to disable navigation buttons at the first and last pages.

Find the full code for this example.

See the live demo for this example.