Search params and URL state
Keep search, filters and pages in the URL with Next.js: the searchParams prop, useSearchParams and its Suspense, router.replace, and Link with a query.
- 01Read the query string on the serverAwait the searchParams prop inside Suspense, and handle values that are missing, repeated or not numbers.
- 02Read and change it in the browserUse useSearchParams with the Suspense it needs, and update the address with router.replace or a Link with a query.
- 03Choose where state livesDecide what belongs in the URL, so it can be shared and bookmarked, and what stays in a component.
One page, a different list for each address
In Dynamic routes, the address picked which tea a page showed: /teas/green. Now the shop wants a search. A visitor types green, and the list shows only the green teas. Where does the word green go? The usual answer on the web is the address itself: /teas?q=green.
The part after the ? is the query string. It holds pairs of a name and a value, written name=value and joined by &: ?q=green&page=2. Each pair is a search param. Next.js gives a page all of them in a prop called searchParams. Here is the shop's list of teas, which reads one search param, q. Like every example in this lesson, it's a real Next.js 16.4.3 project, built with next build and run with next start, and what it sent back was recorded. Open the Browser tab and pick each address above the page.
q from the addressNext.jsapp/teas/page.tsximport { Suspense } from "react";import { findTeas } from "./data"; export default function TeasPage({ searchParams }: PageProps<"/teas">) { return ( <main> <h1>Our teas</h1> <Suspense fallback={<p>Loading teas…</p>}> <TeaList searchParams={searchParams} /> </Suspense> </main> );} async function TeaList({ searchParams }: { searchParams: Promise<{ q?: string | string[] }> }) { const { q } = await searchParams; const teas = findTeas(typeof q === "string" ? q : ""); if (teas.length === 0) return <p>No teas match.</p>; return ( <ul> {teas.map((tea) => ( <li key={tea.slug}>{tea.name}</li> ))} </ul> );}- Green tea
- Mint tea
- Oolong tea
- Jasmine green tea
- Black tea
- Chamomile
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
One file, app/teas/page.tsx, answers all four addresses. Line by line:
- Line 4: Next.js calls
TeasPagewith asearchParamsprop. Likeparams, it's a promise. - Lines 8 to 10 put
TeaListinside Suspense, withLoading teas…as the fallback, so the build has something to show while the list waits for the query string. - Line 16 waits for
searchParamsand takesqout of it. Line 17 keeps the teas whose name containsq, withfindTeasfromapp/teas/data.ts. A name can be given twice, soqcan be an array; line 17 only searches when it's one string, and shows every tea otherwise. - Line 19 shows
No teas match.when nothing is left.
The Browser tab shows six teas for /teas, Green tea and Jasmine green tea for /teas?q=green, Mint tea for /teas?q=mint, and No teas match. for /teas?q=coffee. The Terminal lists one route, ◐ /teas: the legend says (Partial Prerender) prerendered as static HTML with dynamic server-streamed content. The heading and the fallback were made once, during the build, as the page's shell. The list was made for each request, from its query string. The Network tab shows both in each response's HTML: Loading teas… first, then the list.
/teas?q=mint&page=2 in two parts. The path, /teas, picks the page, as folders always do. The query string after the ? is pairs joined by &, and the page gets them as one object, searchParams.At a library desk, you write a slip: the shelf, and under it a note about which books you want. The librarian reads the whole slip and brings exactly those books. Hand a friend a copy of the slip, and they get the same books.
- In real life: The shelf the slip names: Teas
- In Next.js: The path,
/teas, which picks the page - In real life: The notes under it: only green ones, the second dozen
- In Next.js: The query string,
?q=green&page=2 - In real life: The librarian reading the slip before fetching the books
- In Next.js: The page reading
searchParamson the server - In real life: A copy of the slip you hand a friend
- In Next.js: A shared link: the same address shows the same list
The query string is state you keep in the address. The path picks the page; the search params tell it what to show. A page reads them in the searchParams prop, so the server can make the right HTML, and a visitor can share, bookmark or reload the address and see the same thing.
The searchParams prop
What does the object look like? This page prints the searchParams it gets, as text, with JSON.stringify on line 15. The recorder asked it for seven addresses. Pick each one above the page.
searchParamsNext.jsapp/teas/page.tsximport { Suspense } from "react"; export default function TeasPage({ searchParams }: PageProps<"/teas">) { return ( <main> <h1>Our teas</h1> <Suspense fallback={<p>Loading…</p>}> <ShowSearchParams searchParams={searchParams} /> </Suspense> </main> );} async function ShowSearchParams({ searchParams }: { searchParams: Promise<object> }) { return <p>searchParams: {JSON.stringify(await searchParams)}</p>;}searchParams: {}
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
Each row is one address from that recording, and the object the page printed for it:
| Address | What the page got | What to notice |
|---|---|---|
/teas | {} | No query string: an empty object. |
/teas?q=mint | {"q":"mint"} | One key, one string. |
/teas?q=mint&page=2 | {"q":"mint","page":"2"} | & separates pairs. "2" is a string. |
/teas?q=mint&q=green | {"q":["mint","green"]} | A key given twice becomes an array. |
/teas?q= | {"q":""} | A key with no value is an empty string. |
/teas?q=green%20tea | {"q":"green tea"} | %20 is a space, decoded for you. |
So, when you read a search param:
- It may be missing.
/teasgave{}, soqisundefined. Give it a default. - It's a string.
pageis"2", not2. Turn it into a number withNumber(page)before you count with it. - It may be an array.
?q=mint&q=greengave["mint","green"]. Check withtypeof q === "string", as line 17 of the teas page does. - It's a plain object, not the browser's URLSearchParams: read
qwith destructuring, not.get("q").
Like params, searchParams is a promise, because the query string isn't known during the build: the server receives it with each request. With params, a shop can list its teas ahead of time. A query string can be anything a visitor types, so it can't be listed: the part that reads it waits inside Suspense. Pick each version, and read the Terminal.
app/teas/page.tsxNext.jsapp/teas/page.tsximport { findTeas } from "./data"; export default async function TeasPage({ searchParams }: PageProps<"/teas">) { const { q } = await searchParams; const teas = findTeas(typeof q === "string" ? q : ""); return ( <main> <h1>Our teas</h1> <ul> {teas.map((tea) => ( <li key={tea.slug}>{tea.name}</li> ))} </ul> </main> );}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...Error: Route "/teas": Next.js encountered uncached or runtime data during prerendering. `fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>` prevents the route from being prerendered, blocking the page load and leading to a slower user experience. Ways to fix this: - [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access - [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"` (does not apply to `connection()`) - [block] Set `export const instant = false` to allow a blocking route Learn more: https://nextjs.org/docs/messages/blocking-prerender-dynamic at body (<anonymous>) at html (<anonymous>)To get a more detailed stack trace and pinpoint the issue, try one of the following: - Start the app in development mode by running `next dev`, then open "/teas" in your browser to investigate the error. - Rerun the production build with `next build --debug-prerender` to generate better stack traces.Error occurred prerendering page "/teas". Read more: https://nextjs.org/docs/messages/prerender-errorExport encountered an error on /teas/page: /teas, exiting the build.⨯ Next.js build worker exited with code: 1 and signal: nullLine 4 waits for searchParams before anything is shown. During the build there's no request, so no query string, and nothing to show meanwhile. Next.js stops the build and names the fixes.
Awaited at the top, the build stops: Route "/teas": Next.js encountered uncached or runtime data during prerendering. Awaited inside Suspense, it builds as ◐ /teas. The more of the page that sits outside the Suspense, the more of it is ready in the shell: here, the nav and the heading. The Rendering and caching stage covers request-time data in full.
Only pages get the searchParams prop. Next.js's docs explain why: a layout stays on screen while the visitor moves between the pages inside it, and isn't made again, so it would keep showing old values. A layout that needs the query string uses useSearchParams in a Client Component, next.
useSearchParams and the Suspense it needs
The shop wants a search box that shows what was searched for. A box reacts to typing, so it's a Client Component. Pages start as Server Components, which run only on the server; a file that starts with "use client" marks where the client part begins, and its components, and what it imports, also run in the browser. A Client Component reads the query string with the useSearchParams hook:
"use client";
import { useSearchParams } from "next/navigation";
export default function SearchBox() {
const searchParams = useSearchParams();
return <input name="q" placeholder="Search teas" defaultValue={searchParams.get("q") ?? ""} />;
}
This file is part of the recorded projects below; it runs there, not in this page. Line 3 imports useSearchParams from next/navigation. Line 6 calls it. It returns a read-only URLSearchParams, the browser's own object for a query string, so line 8 reads q with .get("q"): "mint" on /teas?q=mint, or null without one, which ?? "" turns into an empty box.
During the build, there is no query string to give the hook. So, like await searchParams, the component needs a Suspense around it, with something to show meanwhile. Each button is one version of the teas page with this box. Before you click, guess what each build does, and what the HTML for /teas?q=mint holds.
app/teas/page.tsxNext.jsapp/teas/page.tsximport SearchBox from "./search-box"; export default function TeasPage() { return ( <main> <h1>Our teas</h1> <SearchBox /> </main> );}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...Error: Route "/teas": Next.js encountered URL data `useSearchParams()` in a Client Component outside of `<Suspense>`. This blocks prerendering because the value is only available at runtime. Ways to fix this: - [stream] Wrap the component in `<Suspense fallback={...}>` so the hook value streams in after prerendering - [block] Set `export const instant = false` to allow a blocking route Learn more: https://nextjs.org/docs/messages/blocking-prerender-client-hook at <unknown> (app/teas/search-box.tsx:6:24) at main (<anonymous>) at body (<anonymous>) at html (<anonymous>) 4 | 5 | export default function SearchBox() {> 6 | const searchParams = useSearchParams(); | ^ 7 | 8 | return <input name="q" placeholder="Search teas" defaultValue={searchParams.get("q") ?? ""} />; 9 | } { digest: 'CLIENT_HOOK_DYNAMIC'}To get a more detailed stack trace and pinpoint the issue, try one of the following: - Start the app in development mode by running `next dev`, then open "/teas" in your browser to investigate the error. - Rerun the production build with `next build --debug-prerender` to generate better stack traces.Error occurred prerendering page "/teas". Read more: https://nextjs.org/docs/messages/prerender-errorExport encountered an error on /teas/page: /teas, exiting the build.⨯ Next.js build worker exited with code: 1 and signal: nullThe box on line 7 reads the query string, and nothing around it says what to show without one. Next.js stops the build at line 6 of search-box.tsx: URL data useSearchParams() in a Client Component outside of <Suspense>.
Without Suspense, the build stops at line 6 of search-box.tsx: Next.js encountered URL data useSearchParams() in a Client Component outside of <Suspense>. With it, what happens depends on the rest of the page:
- Nothing else reads the request. The page is the same for everyone, so the build makes it once:
○ /teas, (Static) prerendered as static content. Its HTML holds the fallback,Loading the search box…, for every query string. Open the Steps tab: in real Chrome, once the page's JavaScript ran, the box showedmint. The browser drew it. - The list reads
searchParams. The page is now made for each request,◐ /teas, and the server renders the box along with it. The Network tab showsvalue="mint"in the HTML.
/teas?q=mint. Left, nothing else reads the request: the page is ○, its HTML holds the fallback, and the browser draws the box. Right, the list reads searchParams: the page is ◐, and the server makes the box with value="mint" for this request.Each row is one version: what the build does, and what the HTML for /teas?q=mint holds.
| The page | What next build does | What the HTML holds |
|---|---|---|
The box outside Suspense | Stops with an error that names the box's line. | Nothing: there's no build. |
In Suspense, nothing else reads the request | ○ /teas: one page for every query string. | The fallback. The box is drawn in the browser. |
In Suspense, the list reads searchParams | ◐ /teas: a shell, then the rest for each request. | The box, with value="mint", made on the server. |
Which to use? In a page, read the searchParams prop: the server can use the values to load data, and pass them to any component as props. In a Client Component deep in the page, or one a layout uses, call useSearchParams, and put a Suspense as close around it as you can, so the rest stays in the shell.
Updating the URL without a reload
Reading the address is half of it. The box should also change the address as the visitor types, so the list follows. Here is the shop with that box, two teas a page, and links to the other pages. The file shown first is the box. Read it, then open the Steps tab.
app/teas/tea-list.tsximport Link from "next/link";import { findTeas } from "./data"; const perPage = 2; export default async function TeaList({ searchParams }: { searchParams: PageProps<"/teas">["searchParams"] }) { const { q, page } = await searchParams; const query = typeof q === "string" ? q : ""; const current = Number(page) || 1; const found = findTeas(query); const pages = Math.max(1, Math.ceil(found.length / perPage)); const shown = found.slice((current - 1) * perPage, current * perPage); const link = (to: number) => ({ pathname: "/teas", query: query ? { q: query, page: to } : { page: to } }); return ( <> <ul> {shown.map((tea) => ( <li key={tea.slug}>{tea.name}</li> ))} </ul> <p> Page {current} of {pages} </p> {current > 1 && <Link href={link(current - 1)}>Previous</Link>}{" "} {current < pages && <Link href={link(current + 1)}>Next</Link>} </> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
- Green tea
- Mint tea
Page 1 of 3
Next- Focus
- nothing: the page itself
- Scrolled
- 0 px down
Requests the page made during this step (3), without the files it loaded
GET/teas200a whole pageGET/200text/x-component- rsc
- 1
- next-router-prefetch
- 1
- next-router-segment-prefetch
- /_tree
- next-url
- /teas
GET/200text/x-component- rsc
- 1
- next-router-prefetch
- 1
- next-router-segment-prefetch
- /__PAGE__
- next-url
- /teas
Lines 6 to 8 call three hooks from next/navigation: useSearchParams for the query string, usePathname for the path, /teas, and useRouter for the router, which can move to another address from code. On every change, line 23 calls search with the box's text:
- Line 11 copies the current query string into a new
URLSearchParams, which, unlike the read-only one, can change. - Lines 12 and 13 set
qto the text, or delete it when the box is empty. Line 14 deletespage: a new search starts on page 1. - Line 15 turns the pairs back into text, like
q=green. Line 16 callsrouter.replacewith the new address:/teas?q=green, or just/teas.
router.replace is a client-side navigation: Next.js's router changes the address and asks the server only for the new page's data, an RSC payload, instead of loading a whole HTML page (the first look at how Next.js works introduced both). The Steps tab shows it, recorded in real Chrome. Step 1 opens /teas, a page load. Step 2 clicks Next: the address becomes /teas?page=2, with one request, a fetch. Step 3 types green in the box: the address becomes /teas?q=green, page is gone, and the list shows Green tea and Jasmine green tea. Again one fetch, and no page load. The box kept its text and its focus, because the page was never loaded again.
green calls router.replace, which changes the address to /teas?q=green without a reload. The router asks the server for that address, the page reads searchParams, and the new list comes back.The Next link in step 2 comes from app/teas/tea-list.tsx. Its href is an object: a pathname and a query. Link turns it into an address:
<Link href={{ pathname: "/teas", query: { q: "tea", page: 2 } }}>Next</Link>This is a fragment of tea-list.tsx, with the values filled in. The recorded /teas?q=tea page has exactly this link: <a href="/teas?q=tea&page=2">. A link is an ordinary <a>, so it works before the JavaScript loads, and Next.js's router takes over the click once it has.
Why replace and not push? Both navigate the same way. Next.js's docs describe the difference: router.push adds an entry to the browser's history, so Back returns to the address before, while router.replace swaps the current entry. A search box changes the address on every letter; with replace, Back doesn't step through g, gr, gre. A Link pushes, which suits page links: Back goes to the page before. Linking and navigating covers the router in full.
Search and pages for the tea shop
Now the list itself. It reads two search params, q and page, shows two teas a page, and links to the pages before and after. Open the Browser tab, and click Next and Previous: each link switches to its recorded page.
app/teas/tea-list.tsximport Link from "next/link";import { findTeas } from "./data"; const perPage = 2; export default async function TeaList({ searchParams }: { searchParams: PageProps<"/teas">["searchParams"] }) { const { q, page } = await searchParams; const query = typeof q === "string" ? q : ""; const current = Number(page) || 1; const found = findTeas(query); const pages = Math.max(1, Math.ceil(found.length / perPage)); const shown = found.slice((current - 1) * perPage, current * perPage); const link = (to: number) => ({ pathname: "/teas", query: query ? { q: query, page: to } : { page: to } }); return ( <> <ul> {shown.map((tea) => ( <li key={tea.slug}>{tea.name}</li> ))} </ul> <p> Page {current} of {pages} </p> {current > 1 && <Link href={link(current - 1)}>Previous</Link>}{" "} {current < pages && <Link href={link(current + 1)}>Next</Link>} </> );}- Green tea
- Mint tea
Page 1 of 3
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
- Line 7 waits for
searchParamsand takesqandpage. Line 8 keepsqonly when it's one string. - Line 9 turns
pageinto a number.Number("2")is2. A missing page, or text likeabc, givesNaNor0, and|| 1turns those into page 1. - Line 11 counts the pages, and line 12 takes this page's two teas with slice.
- Line 13 builds each link's
href, keepingqso the search survives a change of page. Lines 25 and 26 show Previous and Next only when there's a page to go to.
The recording shows the result for every address the box and the links lead to: /teas?q=tea has five teas, so Page 1 of 3, and its Next link is /teas?q=tea&page=2. /teas?page=1 shows the same teas as /teas. Each of these addresses can be shared: whoever opens it gets the same page of the same search, made on the server.
URL state or component state?
Not everything belongs in the address. State in a component, with useState, lives in the browser, for as long as the component is on screen. State in the URL lives in the address. Each row asks one question of both:
| Question | In the URL | In component state |
|---|---|---|
| Can a visitor share it or bookmark it? | Yes. It's part of the address. | No. It's gone when the page closes. |
| Does it survive a reload? | Yes. | No. It starts again from its first value. |
| Can the server read it? | Yes: searchParams in a page. | No. It only exists in the browser. |
| What does changing it cost? | With Link or router.replace, as in this lesson, a navigation: the router asks the server for the new page's data. | A re-render in the browser, with no request. |
There is a third way, for URL state only the browser needs. Next.js's docs say you can also call the browser's own window.history.pushState and window.history.replaceState. They change the address without a reload, and useSearchParams and usePathname follow the change. The docs' example uses it for a sort order that a Client Component applies to a list it already has.
A good rule: if someone would want it in a link they share, or the server needs it to make the page, put it in the URL. If it's private, short-lived or changes many times a second, keep it in the component. Sort these:
- The text the list is filtered by
- Which page of results is shown
- Sort by price or by name
- Whether a dropdown menu is open
- Which tea card the mouse is over
- A review the visitor is still typing
Sort each piece of the tea shop's state: should it live in the URL, or in component state?
Common mix-ups
- "
searchParamsis aURLSearchParams." No. The prop is a promise of a plain object. OnlyuseSearchParamsreturns aURLSearchParams, with.get(). - "
?page=2gives the number 2." No. The recorded page got"2", a string. UseNumber(page). - "Each name has one value." No.
?q=mint&q=greengave an array. - "
useSearchParamsworks anywhere." It works in Client Components, inside aSuspense. Without one, the recorded build stopped. - "Changing the query string reloads the page." No. In real Chrome,
router.replaceand aLinkchanged it with a fetch, and the box kept its text. - "Search params make the whole page dynamic." Only the part that waits for them, inside
Suspense. The heading and the nav were in the shell, made during the build.
Practice exercises
These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.
The inspecting page from this lesson is asked for this address. What is sort in searchParams?
/teas?sort=price&sort=name["price","name"], an array. A name given twice gets an array of its values, in order. The recorded page printed searchParams: {"sort":["price","name"]}.
For this address, what type is page in searchParams?
/teas?q=mint&page=2A string, "2". Everything in an address is text. The recorded page printed {"q":"mint","page":"2"}, so page needs Number(page) before you count with it.
Which address does this link point at?
<Link href={{ pathname: "/teas", query: { q: "tea", page: 2 } }}>Next</Link>/teas?q=tea&page=2. The recorded /teas?q=tea page has a Next link with exactly this href.
A visitor opens /teas?q=coffee. What does the list part of this page show?
import { Suspense } from "react";
import { findTeas } from "./data";
export default function TeasPage({ searchParams }: PageProps<"/teas">) {
return (
<main>
<h1>Our teas</h1>
<Suspense fallback={<p>Loading teas…</p>}>
<TeaList searchParams={searchParams} />
</Suspense>
</main>
);
}
async function TeaList({ searchParams }: { searchParams: Promise<{ q?: string | string[] }> }) {
const { q } = await searchParams;
const teas = findTeas(typeof q === "string" ? q : "");
if (teas.length === 0) return <p>No teas match.</p>;
return (
<ul>
{teas.map((tea) => (
<li key={tea.slug}>{tea.name}</li>
))}
</ul>
);
}
No teas match. No tea's name contains coffee, so teas is empty and line 19 returns that paragraph. The recording of /teas?q=coffee shows it.
This is app/teas/page.tsx, and SearchBox is the box from this lesson. next build stops. What do you change?
import SearchBox from "./search-box";
export default function TeasPage() {
return (
<main>
<h1>Our teas</h1>
<SearchBox />
</main>
);
}
import { Suspense } from "react";
import SearchBox from "./search-box";
export default function TeasPage() {
return (
<main>
<h1>Our teas</h1>
<Suspense fallback={<p>Loading the search box…</p>}>
<SearchBox />
</Suspense>
</main>
);
}
Wrap <SearchBox /> in Suspense. The recorded build stopped with: Next.js encountered URL data useSearchParams() in a Client Component outside of <Suspense>. With the Suspense, it built as ○ /teas.
A visitor opens /teas?q=mint. Where the box will go, what does the HTML from the server hold?
import { Suspense } from "react";
import SearchBox from "./search-box";
export default function TeasPage() {
return (
<main>
<h1>Our teas</h1>
<Suspense fallback={<p>Loading the search box…</p>}>
<SearchBox />
</Suspense>
</main>
);
}
The fallback, Loading the search box…. Nothing on the page reads the request, so the build made one HTML page, ○ /teas, for every query string. In real Chrome, the box appeared after the JavaScript ran.
Check your understanding
For each question, ask who has the query string at that moment: the build, the server on a request, or the browser.
Question 1 of 7What does
searchParamshold for/teas?q=mint&page=2?Choose an answer to see the explanation.
Question 2 of 7A page awaits
searchParamsat the top, with noSuspense. What doesnext builddo with Cache Components on?Choose an answer to see the explanation.
Question 3 of 7Why does a Client Component that calls
useSearchParamsneed aSuspensearound it?Choose an answer to see the explanation.
Question 4 of 7The page reads nothing else from the request, and the search box is in
Suspense. What is in the HTML for/teas?q=mint?Read the code, then predictTSXimport { Suspense } from "react"; import SearchBox from "./search-box"; export default function TeasPage() { return ( <main> <h1>Our teas</h1> <Suspense fallback={<p>Loading the search box…</p>}> <SearchBox /> </Suspense> </main> ); }Choose an answer to see the explanation.
Question 5 of 7Which line makes the search box change the address?
Read the code, then predictTSX"use client"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; export default function SearchBox() { const searchParams = useSearchParams(); const pathname = usePathname(); const router = useRouter(); function search(text: string) { const params = new URLSearchParams(searchParams); if (text) params.set("q", text); else params.delete("q"); params.delete("page"); const query = params.toString(); router.replace(query ? `${pathname}?${query}` : pathname); } return ( <input placeholder="Search teas" defaultValue={searchParams.get("q") ?? ""} onChange={(event) => search(event.target.value)} /> ); }Choose an answer to see the explanation.
Question 6 of 7What does
<Link href={{ pathname: "/teas", query: { page: 2 } }}>render?Choose an answer to see the explanation.
Question 7 of 7Where should the open or closed state of a dropdown menu live?
Choose an answer to see the explanation.
Key takeaways
- The query string, after the
?, is state kept in the address. Pages get it in thesearchParamsprop. searchParamsis a promise of a plain object. Values are strings, or arrays when a name repeats, and may be missing.- A query string isn't known during the build, and the server receives it with each request, so the part that reads it waits inside
Suspense; the rest stays in the shell. - Client Components read it with
useSearchParams, insideSuspense, and change it withrouter.replaceor aLinkwith aquery, without a reload. - Put state in the URL when it should be shared, survive a reload, or be read by the server. Keep the rest in components.
Remember the one-liner.
If someone should be able to share it, put it in the URL.
Coming next: Route groups and private folders. Folders in brackets group routes without changing the address, and folders that start with _ stay out of routing.