light/dark

Implementing Search with URL search params
8/26/2024·6 min read·10 views

Next.js setup

Let's begin by creating a Next.js application. We'll build a basic search bar utilizing URL search parameters to filter through our mock data, which is a list of blog titles generated by Gemini.

// src/app/lib/data.ts
export const blogTitles = [
"AI Revolutionizes Content Creation",
"The Future of Web Development: Serverless Architecture",
"Mastering Machine Learning with Python",
"The Dark Side of Social Media Algorithms",
"Is Quantum Computing the Next Big Thing?",
"Building a Real-Time Chat App with WebSockets",
"How I Overcame My Fear of Public Speaking",
"Finding Your Passion: A Step-by-Step Guide",
"The Importance of Self-Care in a Busy World",
"Minimalist Living: Declutter Your Life",
"Sustainable Fashion: Ethical Choices for a Better Planet",
"The Art of Travel Hacking: Tips and Tricks",
"The Power of Storytelling: Connecting with Your Audience",
"Exploring the Human Condition Through Poetry",
"The Art of World-Building: Creating Immersive Universes",
"Writing Comedy: The Secrets of Stand-Up Success",
"The Ultimate Guide to Punny Titles",
"How to Write a Killer Horror Story",
];

Next, we'll build two essential components: Search.tsx and Blogs.tsx. The Search.tsx component will handle user input and trigger the search functionality with URL search params. The Blogs.tsx component will display the search results based on the user's query.

First let's implement a simple search bar.

// src/app/components/Search.tsx
const Search = () => {
return (
<div className="">
<input
placeholder="query..."
className="w-full border-gray-200 text-lg"
/>
</div>
);
};
export default Search;

Capturing the user's input

To capture the user's input, we use the use client directive to convert it Client Component, create a handleSearch function and add a onChange listener to the <input> element.

// src/app/components/Search.tsx
"use client";
const Search = () => {
function handleSearch(term: string) {
console.log(term);
}
return (
<div className="">
<input
placeholder="query..."
className="w-full border-gray-200 text-lg"
onChange={(e) => {
handleSearch(e.target.value);
}}
/>
</div>
);
};
export default Search;

Updating the URL

Next, we import useSearchParams,usePathname, and useRouter hooks from next/navigation.

// src/app/components/Search.tsx
import { useSearchParams, usePathname, useRouter } from "next/navigation";

Assign useSearchParams hook to a variable, and create a new URLSearchParams instance using the new variable we just created inside handleSearch .

// src/app/components/Search.tsx
export default function Search() {
const searchParams = useSearchParams();
function handleSearch(term: string) {
const params = new URLSearchParams(searchParams);
}
// ...
}

The useSearchParams is a Client Component hook that lets you read the current URL's query string, while URLSearchParams is a Web API that provides utility methods for manipulating the URL query parameters. Instead of creating a complex string literal, we can use it to get the params string like ?page=1&query=a.

Next, we set the params string based on the user’s input. If the input is empty, delete it.

// src/app/components/Search.tsx
// ...
function handleSearch(term: string) {
const params = new URLSearchParams(searchParams);
if (term) {
params.set("query", term);
} else {
params.delete("query");
}
}
// ...

We use the replace method from useRouter() inside handleSearch to updates the URL with the user's search data.

Synchronizing the URL and input

To ensure the input field is in sync with the URL, pass a defaultValue to input by reading from searchParams

// src/app/components/Search.tsx
// ...
<input
className="w-full border-gray-200 text-lg"
placeholder="query..."
onChange={(e) => {
handleSearch(e.target.value);
}}
defaultValue={searchParams.get("query")?.toString()}
/>
// ...

We use defaultValue instead of value because we are not relying on React to handle the input's state. The native input element handles its own state. This distinction in state management is the concept of Controlled vs. Uncontrolled components. In our scenario, we're adopting an Uncontrolled component approach.

Updating the UI

In the Titles component, we import our mock data and modify the component to accept a query prop. This allows us to filter titles based on the search query. The filtering mechanism dynamically displays results that match the input. To simulate real-world data fetching from an API or ORM, you can replace the mock data with actual data retrieval logic.

import { blogTitles } from "../lib/data";
const Titles = ({ query }: { query: string }) => {
const filteredTitles = query
? blogTitles.filter((blog) =>
blog.toLowerCase().includes(query.toLowerCase())
)
: blogTitles; // Display all titles if no query is provided
return (
<div>
{filteredTitles.map((blog) => (
<p key={blog}>{blog}</p>
))}
</div>
);
};
export default Titles;

You might have noticed that we utilized two different techniques to extract search parameters: the useSearchParams() hook and the searchParams prop. The selection between these approaches depends on whether you're working on the client or the server. The Search component, being a client-side component, employs useSearchParams(). Conversely, the Titles component, a server-side component, leverages the searchParams prop from the page.tsx file.

Debouncing

We use debounce to limits the rate at which a function can fire. In our case, you only want to query the database when the user has stopped typing. Debouncing involves setting a timer when an event occurs. If another event triggers before the timer expires, it resets the countdown. Only when the timer finishes without being reset does the debounced function execute. This prevents excessive function calls, especially in scenarios like typing.

We install the use-debounce library to install debounce.

npm i use-debounce

We modify the handleSearch function to incorporate a debounce mechanism to optimize performance.

// src/app/componenents/Search.tsx
// ...
const handleSearch = useDebouncedCallback((term) => {
const params = new URLSearchParams(searchParams);
if (term) {
params.set("query", term);
} else {
params.delete("query");
}
replace(`${pathname}?${params.toString()}`);
}, 300);
// ...

This code ensures that the code within the handleSearch function executes 300 milliseconds after the user has finished typing.

Summary

To summarize, we successfully implemented search with URL search parameters by:

Capturing user input: We monitored user input from the search field.

Updating the URL: We dynamically updated the URL to include the search query as a parameter.

Synchronizing input and URL: We ensured that the input field and URL remained consistent, reflecting the current search term.

Updating the UI: We refreshed the user interface to display results based on the search query.

Additionally, we incorporated a debounce mechanism to enhance performance and prevent excessive function calls, resulting in a more responsive and efficient search experience. Thank you for reading!