Summary
In an App Router application built with output: "export", useSearchParams() returns an empty ReadonlyURLSearchParams when a prerendered page is opened with query parameters.
For example, when directly opening:
/panel/settings?tab=server
the browser address remains correct, but:
useSearchParams().get("tab")
returns null.
The same problem affects client-side navigation to query-dependent pages, such as:
/panel/logs/view?log=latest.log
The generated static HTML/RSC payload contains build-time empty search params. During hydration, vinext restores that embedded navigation context and treats it as authoritative instead of using the current browser window.location.search.
Environment
- vinext: 1.0.0-beta.6
- Vite: 8.2.0
- React: 19.2.8
- React DOM: 19.2.8
- App Router
output: "export"
- Production build
- Static files served by an application server
This does not reproduce with vinext dev.
Reproduction
Configure static export:
// next.config.ts
const nextConfig = {
output: "export",
};
export default nextConfig;
Create a client page that reads a query parameter:
"use client";
import { useSearchParams } from "next/navigation";
export default function Page() {
const searchParams = useSearchParams();
return (
<p data-testid="query-value">
{searchParams.get("value") ?? "missing"}
</p>
);
}
Build the application:
Serve the generated static files, then directly open:
Actual behavior
The page renders:
The browser URL still contains:
Logging the values shows the mismatch:
console.log({
locationSearch: window.location.search,
hookSearch: searchParams.toString(),
});
Result:
{
locationSearch: "?value=expected",
hookSearch: ""
}
In the generated static HTML/RSC payload, the embedded navigation state contains
empty build-time search params:
Expected behavior
After hydration, useSearchParams() should reflect the current browser URL:
searchParams.get("value") === "expected"
A single statically exported page is expected to be reusable for arbitrary query strings, with client components reading the current query from the browser.
If vinext requires a Suspense boundary for this static-rendering bailout, it should follow Next.js behavior rather than silently hydrating the page with an empty query snapshot.
Possible root cause
The initial browser RSC bootstrap restores the navigation context from the embedded static RSC payload:
|
if (rsc.nav) { |
|
restoreHydrationNavigationContext(rsc.nav.pathname, rsc.nav.searchParams, params); |
|
} |
For static exports, rsc.nav.searchParams was produced at build time and is empty because no request URL existed during prerendering.
The navigation shim then prioritizes that restored navigation context:
|
function getSearchParamsSnapshot(): ReadonlyURLSearchParams { |
|
if (getNavigationContext()) return getServerSearchParamsSnapshot(); |
Consequently, its fallback based on the current browser URL is never used, even though window.location.search contains the correct value.
The branch that fetches a fresh RSC payload restores the context from window.location.search, but the embedded-RSC static export branch does not.
Suggested fix
When hydrating an embedded static-export RSC payload, use the visible browser query string instead of the build-time query snapshot.
For example:
restoreHydrationNavigationContext(
rsc.nav.pathname,
- rsc.nav.searchParams,
+ window.location.search,
rsc.nav.params,
);
Alternatively, static generation could include an explicit flag telling the navigation shim to source search params from window.location.search.
This should preserve the embedded pathname and route params while allowing one static artifact to work with arbitrary query strings.
Related issues and PRs
PR #2882 is included in 1.0.0-beta.6, but the problem still occurs in this static-export hydration scenario.
PR #2883 appears to address the internal valued _rsc= cache-busting query through redirects and rewrites. It does not address application query parameters being replaced by the build-time embedded navigation snapshot.
Additional symptoms
Query-dependent application behavior breaks because the hook reports missing parameters:
- A settings page opened with
?tab=server falls back to its default tab.
- A log detail page opened with
?log=latest.log treats the log name as missing and redirects to the log list.
- Query-dependent client state may trigger redirect or navigation loops.
The HTTP server preserves the original application query string when serving the exported HTML and RSC files. The query is still visible in the browser URL, so the loss appears to happen in vinext's client hydration/navigation state.
Summary
In an App Router application built with
output: "export",useSearchParams()returns an emptyReadonlyURLSearchParamswhen a prerendered page is opened with query parameters.For example, when directly opening:
the browser address remains correct, but:
returns
null.The same problem affects client-side navigation to query-dependent pages, such as:
The generated static HTML/RSC payload contains build-time empty search params. During hydration, vinext restores that embedded navigation context and treats it as authoritative instead of using the current browser
window.location.search.Environment
output: "export"This does not reproduce with
vinext dev.Reproduction
Configure static export:
Create a client page that reads a query parameter:
Build the application:
Serve the generated static files, then directly open:
Actual behavior
The page renders:
The browser URL still contains:
Logging the values shows the mismatch:
Result:
In the generated static HTML/RSC payload, the embedded navigation state contains
empty build-time search params:
Expected behavior
After hydration,
useSearchParams()should reflect the current browser URL:A single statically exported page is expected to be reusable for arbitrary query strings, with client components reading the current query from the browser.
If vinext requires a Suspense boundary for this static-rendering bailout, it should follow Next.js behavior rather than silently hydrating the page with an empty query snapshot.
Possible root cause
The initial browser RSC bootstrap restores the navigation context from the embedded static RSC payload:
vinext/packages/vinext/src/server/app-browser-entry.ts
Lines 1423 to 1425 in 365c604
For static exports,
rsc.nav.searchParamswas produced at build time and is empty because no request URL existed during prerendering.The navigation shim then prioritizes that restored navigation context:
vinext/packages/vinext/src/shims/navigation.ts
Lines 1874 to 1875 in 365c604
Consequently, its fallback based on the current browser URL is never used, even though
window.location.searchcontains the correct value.The branch that fetches a fresh RSC payload restores the context from
window.location.search, but the embedded-RSC static export branch does not.Suggested fix
When hydrating an embedded static-export RSC payload, use the visible browser query string instead of the build-time query snapshot.
For example:
Alternatively, static generation could include an explicit flag telling the navigation shim to source search params from
window.location.search.This should preserve the embedded pathname and route params while allowing one static artifact to work with arbitrary query strings.
Related issues and PRs
useSearchParams/searchParamsstatic-bailout to client rendering not implemented #1524 — App RouteruseSearchParams/searchParamsstatic bailoutPR #2882 is included in
1.0.0-beta.6, but the problem still occurs in this static-export hydration scenario.PR #2883 appears to address the internal valued
_rsc=cache-busting query through redirects and rewrites. It does not address application query parameters being replaced by the build-time embedded navigation snapshot.Additional symptoms
Query-dependent application behavior breaks because the hook reports missing parameters:
?tab=serverfalls back to its default tab.?log=latest.logtreats the log name as missing and redirects to the log list.The HTTP server preserves the original application query string when serving the exported HTML and RSC files. The query is still visible in the browser URL, so the loss appears to happen in vinext's client hydration/navigation state.