Skip to content

useSearchParams returns empty values after hydration in static exports opened with query params #2928

Description

@NriotHrreion

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:

vinext build

Serve the generated static files, then directly open:

/example?value=expected

Actual behavior

The page renders:

missing

The browser URL still contains:

?value=expected

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:

searchParams: []

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions