Next.js Web App Search Functionality

2024-01-14
By: O. Wolfson

This article will guide you through setting up the project, creating the necessary components, and implementing search functionality with URL parameters.

Here is a deployed version of the blog application: https://next-search-params-cyan.vercel.app/blog

Code on GitHub: https://github.com/owolfdev/next-search-params

Prerequisites

  • Basic understanding of React, Next.js, and TypeScript.
  • Node.js and npm installed.

Setup the Next.js 14 Project

Create a New Next.js App:

bash
npx create-next-app@latest nextjs-blog-app --typescript
cd nextjs-blog-app

Step 1: Setting Up Data

Create a posts.ts file inside the data directory with your blog posts.

typescript
// data/posts.ts
export const blogPosts = [
  {
    slug: "post-one",
    title: "First Blog Post",
    content: "This is the content of the first post.",
  },
  // ... other posts
];

Step 2: Creating the Blog Component

Create the Blog component in app/blog/page.tsx. This component will display all blog posts and include a search bar.

typescript
// app/blog/page.tsx
import React from "react";
import Link from "next/link";
import { blogPosts } from "@/data/posts/posts.js";
import SearchBar from "@/components/SearchBar";

const BlogPage = ({
  searchParams,
}: {
  searchParams: { [key: string]: string | string[] | undefined };
}) => {
  const value = searchParams.slug;
  const searchQuery = searchParams.search?.toString().toLowerCase() || "";
  const filteredPosts = blogPosts.filter((post) =>
    post.title.toLowerCase().includes(searchQuery)
  );
  return (
    <div className="flex flex-col gap-8">
      <div>Blog Page</div>

      <div>
        <SearchBar />
      </div>

      <div>
        {filteredPosts.map((post) => {
          return (
            <div key={post.slug}>
              <Link href={`/blog/${post.slug}`}>{post.title}</Link>
            </div>
          );
        })}
      </div>
    </div>
  );
};

export default BlogPage;

Props Structure and Data Import:

  • The BlogPage component accepts searchParams as a prop. This object contains URL search parameters, key-value pairs corresponding to the query parameters in the URL.
  • Blog post data is imported from @/data/posts/posts.js.

Search Functionality:

  • The searchQuery is extracted from searchParams. If the search parameter exists, it is converted to a string and made lowercase. If it doesn't exist, it defaults to an empty string.
  • The blogPosts array is filtered to include only those posts whose titles contain the searchQuery. This filtering is case-insensitive, as both the titles and the search query are converted to lowercase.

Rendering:

  • The component returns a JSX structure consisting of a div with a flex column layout.
  • It renders a SearchBar component, which handles the updating of the search parameters in the URL.
  • Below the SearchBar, it maps over filteredPosts to display each post. Each post is wrapped in a Link component to create a clickable element that navigates to the individual blog post page (/blog/${post.slug}).

Step 3: Implementing the SearchBar Component

Create the SearchBar component in app/components/SearchBar.tsx.

typescript
// app/components/SearchBar.tsx
"use client";

import { useState, useEffect } from "react";
import { useSearchParams } from "next/navigation";
import { useRouter, usePathname } from "next/navigation";

export default function SearchBar() {
  const searchParams = useSearchParams();
  const [inputValue, setInputValue] = useState("");
  const router = useRouter();
  const pathname = usePathname();

  // Update the input value when it changes
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setInputValue(event.target.value);
  };

  // Optionally, submit the form and update the URL
  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    if (inputValue === "") {
      router.push(pathname);
      return;
    }
    router.push(`${pathname}?search=${encodeURIComponent(inputValue)}`);
  };

  // Initialize the input value with the current search parameter
  useEffect(() => {
    const searchQuery = searchParams.get("search");
    if (searchQuery) {
      setInputValue(searchQuery);
    }
  }, [searchParams]);

  return (
    <form onSubmit={handleSubmit} className="flex flex-col gap-2">
      <div>
        <input
          type="text"
          value={inputValue}
          onChange={handleInputChange}
          placeholder="Search..."
          className="border border-gray-300 rounded-md p-2 text-black"
        />
      </div>
      <div>
        <button type="submit" className="border px-1 rounded">
          Search
        </button>
      </div>
    </form>
  );
}

Imports and Client-Side Directive:

  • Hooks useState, useEffect, useSearchParams, useRouter, and usePathname are imported from React and next/navigation.

State and Hooks:

  • useSearchParams is used to access the current URL's search parameters.
  • useState is used to maintain inputValue, the current state of the search input field.
  • useRouter provides navigation functionality, and usePathname gives access to the current pathname of the URL.

Form Submission:

  • handleSubmit prevents the default form submission and handles the search functionality.

  • If inputValue is not empty, it navigates to the current pathname with inputValue as a query parameter (?search=).

  • If inputValue is empty, it navigates back to the current pathname without any search parameters.

  • Enhanced Navigation and URL Management:

    • The SearchBar component demonstrates an effective way of managing URL search parameters in a Next.js 14 application. By using useSearchParams for reading and useRouter for updating the URL, the component can dynamically control the browser's URL based on user interaction.
    • This approach enables a more interactive and responsive search experience. As users type and submit their queries, the URL updates immediately, reflecting the current search state. This makes the search feature more intuitive and user-friendly.

Step 4: Creating the Blog Post Component

Create a component for individual blog posts in app/blog/[slug]/page.tsx.

typescript
// app/blog/[slug]/page.tsx
import React from "react";
import path from "path";
import { blogPosts } from "@/data/posts/posts.js";

export async function generateStaticParams() {
  const posts = blogPosts;

  return posts.map((post: any) => ({
    slug: post.slug,
  }));
}

export default async function BlogPostPage({
  params,
}: {
  params: { slug: string };
}) {
  const post = blogPosts.find((p) => p.slug === params.slug);
  return (
    <div>
      <div>{post?.title}</div>
      <div>{post?.content}</div>
    </div>
  );
}

This function and the component together handle the rendering and static generation of individual blog post pages based on dynamic URL segments.

  • Role in the Blog Application: This component is a crucial part of the blog application, enabling the functionality of viewing individual blog posts. Each post has its own unique URL based on its slug, and the content is statically generated at build time for optimal performance and SEO benefits.

generateStaticParams Function

  • Purpose: This function is used in combination with dynamic route segments ([slug] in this case) to statically generate routes at build time.
  • Implementation: It takes the blogPosts data (imported from @/data/posts/posts.ts) and maps over it to return an array of objects, each containing a slug property. These objects represent

the dynamic segments needed for each route.

  • Static Generation: During the build process, Next.js calls generateStaticParams and uses its return value to statically generate a page for each slug in the blogPosts array.

BlogPostPage Component

  • Functionality: This component is responsible for rendering the content of an individual blog post.
  • Parameters: It receives params as a prop, which includes the slug of the current blog post.
  • Data Fetching: The component uses the slug to find the corresponding post from the blogPosts data. It then renders the title and content of the post.
  • Handling Non-Existent Posts: If a post with the given slug does not exist (e.g., when accessing a URL with a slug that hasn't been generated), the component will safely render nothing for the title and content, avoiding any errors.

Conclusion

This article covers setting up a blog application using Next.js 14 with TypeScript, focusing on search functionality. We've implemented a blog page with a search bar and individual blog post pages, utilizing the new App Router API in Next.js 14. The useRouter hook is used to manage URL parameters for search functionality, demonstrating a modern approach to web application development with Next.js.