Search Forms Driven by URL Search Params

Drive a search form from URL search params: submit a GET form, read the searchParams prop on the server, and render shareable results.

5 min read

Search forms driven by URL search params put the query in the address bar. The form submits with a GET request, the browser encodes the fields into the URL, and the page reads them back from its searchParams prop. The result is a shareable URL that survives a refresh and works with the back button.

Start with a plain GET form. A form defaults to the GET method, and a form without an action submits to the current route, so placing it on the search page keeps everything in one file.

App.tsxApp.tsx
// app/search/page.tsx
export default function SearchPage() {
  return <form>
    <label htmlFor="query">Search</label>
    <input id="query" name="query" />
    <button type="submit">Search</button>
  </form>
}

When a visitor submits, the browser navigates to /search?query=... with the input value in the URL. That is a full page load, and no JavaScript is involved in it.

Read the query on the server

The page receives the query through its searchParams prop. In the App Router this prop is a Promise, so await it before reading the value. This version of the file replaces the one above, with the form kept alongside the results.

App.tsxApp.tsx
// app/search/page.tsx
export default async function SearchPage({
  searchParams,
}: {
  searchParams: Promise<{ query?: string | string[] }>
}) {
  const { query } = await searchParams
  const term = (Array.isArray(query) ? query[0] : query) ?? ''
  return <p>Results for {term}</p>
}

The value can be a string, an array, or missing, because the same key can repeat. Normalize it to one string, then pass it to your search function and render the results in place of the placeholder paragraph.

Once normalized, the term is a plain string you can hand to a database query, an ORM, or a search service.

An empty submission produces an empty string. Decide whether that means show everything or show a prompt, and handle it before the search call runs.

Why URL state wins

Keeping the query in the URL instead of component state makes the results shareable and bookmarkable. A visitor can copy the link, send it to someone, or hit the back button to return to an earlier search.

It also works before JavaScript loads. The GET form posts natively, so search never depends on hydration to function.

Server-rendered search also keeps the first paint fast, because the query is available during render and the results arrive with the page instead of after a second client fetch. A visitor on a slow network still gets a complete page on the first response.

Build it without a full reload

The Form component from next/form replaces the native form to add client-side navigation and prefetching. The URL behavior is identical; only the transition gets faster.

For live, type-as-you-go search, read the URL on the client with useSearchParams and update it as the visitor types, debounced so one keystroke does not fire a request.

That hook has a rule attached. On a prerendered route it pushes the Client Component tree up to the nearest Suspense boundary into client-side rendering, and a production build of a static page that calls it without one fails with "Missing Suspense boundary with useSearchParams". Wrap the component so the rest of the page still prerenders.

See the Next.js Form component explained for that component, and usePathname and useSearchParams explained for reading the URL on the client in a live search.

Rune AI

Rune AI

Key Insights

  • Use a GET form so the query lands in the URL.
  • Await the searchParams Promise in the page.
  • Normalize the value before using it, since it can be an array.
  • Handle an empty query by showing all results or a prompt.
  • Reach for the Form component for client-side navigation.
RunePowered by Rune AI

Frequently Asked Questions

Why store the query in the URL?

So results are shareable, bookmarkable, and consistent with the back button. A visitor can copy the link or return to an earlier search.

Is searchParams a Promise?

Yes. In the App Router the searchParams prop is a Promise, so await it before reading any value from it.

What type does a searchParams value have?

It can be a string, an array of strings, or undefined, because the same key can appear more than once in a URL.

Conclusion

A search form driven by URL search params submits with GET, encodes the query into the URL, and reads it back from the awaited searchParams prop on the server. The URL becomes the single source of truth for what the page shows.