Add Search to the Contacts Query
Add Search to the Contacts Query
Extend the query function
Open lib/contacts.ts. Replace getContacts with a version that accepts
an optional search term:
export type ContactQuery = {
q?: string;
};
export async function getContacts(opts: ContactQuery = {}): Promise<Contact[]> {
const conditions: string[] = [];
const params: any[] = [];
if (opts.q) {
params.push(`%${opts.q.toLowerCase()}%`);
conditions.push(
`(lower(c.name) LIKE $${params.length} OR lower(coalesce(c.email,'')) LIKE $${params.length} OR lower(coalesce(c.phone,'')) LIKE $${params.length} OR lower(coalesce(c.company,'')) LIKE $${params.length})`
);
}
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
const result = await pool.query(
`SELECT c.* FROM contacts c ${where} ORDER BY lower(c.name) ASC`,
params
);
return result.rows;
}
A few things worth understanding here:
paramsand$1,$2, … are parameterized query placeholders —pg
substitutes them safely, so a search term can never be used to inject
SQL, even if it contains quotes or SQL keywords.coalesce(c.email,'')turns aNULLemail into an empty string before
comparing, so contacts with no email don’t cause theLIKEto silently
skip them.- The search matches name, email, phone, or company — whichever the
learner remembers about the person they’re looking for.
Add the search bar UI
Create components/SearchBar.tsx:
export default function SearchBar({
action,
defaultValue,
}: {
action: string;
defaultValue?: string;
}) {
return (
<form className="search-form" action={action} method="GET">
<input
type="search"
name="q"
placeholder="Search contacts…"
defaultValue={defaultValue}
autoComplete="off"
/>
</form>
);
}
This is a plain HTML form with method="GET". Submitting it navigates the
browser to action?q=<value> — no onChange, no fetch, no client
JavaScript at all. The browser itself turns form input into a URL query
string.
Read the query param on the Home page
Update app/page.tsx:
import { getContacts } from "@/lib/contacts";
import ContactList from "@/components/ContactList";
import SearchBar from "@/components/SearchBar";
export const dynamic = "force-dynamic";
export default async function HomePage({
searchParams,
}: {
searchParams: { q?: string };
}) {
const q = searchParams.q || "";
const contacts = await getContacts({ q });
return (
<>
<div className="page-heading">
<h1>All Contacts</h1>
<div className="meta">{contacts.length} {contacts.length === 1 ? "contact" : "contacts"}</div>
</div>
<SearchBar action="/" defaultValue={q} />
<ContactList
contacts={contacts}
emptyTitle={q ? "No matches" : "No contacts yet"}
emptyBody={q ? `No contacts match "${q}".` : "Your address book is empty."}
/>
</>
);
}
Every Server Component page in the App Router automatically receives a
searchParams prop reflecting the current URL’s query string — you don’t
need any routing library or hook to read it.
Test it
Search for “grace”. The URL should change to /?q=grace, and the list
should shrink to just Grace Hopper. Clear the search box and submit again —
you’re back to all three contacts. Try refreshing the page while a search
is active: the search box still shows your term, because defaultValue is
reading it straight from the URL.
Checkpoint
Searching filters the contact list by name, email, phone, or company, and
the result is reflected in the URL and survives a page refresh.