[Ctrl J]
[Ctrl K]
light/dark
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.tsexport 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.tsxconst Search = () => {return (<div className=""><inputplaceholder="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=""><inputplaceholder="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.tsximport { 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.tsxexport 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// ...<inputclassName="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 providedreturn (<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!