cloudflare / cloudflare/vinext
App Router search-only navigation can fail to commit when removing the last query parameter
- Dominant language
- TypeScript
- Stars
- 8.8k
- Forks
- 406
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 120
Description
## Summary
Under vinext 1.0.0-beta.5, an App Router transition from a filtered URL such as `/top?provider=8` back to the same pathname without search parameters can fail to commit. The RSC request for `/top` reaches the Worker and returns 200, but the client remains in the provider-filtered state or falls into vinext's default client error boundary.
This looks related to the stale search-param cache behavior tracked in #1621, but it is occurring on beta.5 after that issue was closed.
## Reproduction shape
The page is dynamic and reads `searchParams`. A client filter uses `useSearchParams()`, optimistic state, and `router.push()`:
```tsx
"use client";
import { useOptimistic, useTransition } from "react";
import { useRouter, useSearchParams } from "next/navigation";
export function ProviderToggle() {
const router = useRouter();
const routeSearchParams = useSearchParams();
const [isPending, startTransition] = useTransition();
const [optimisticQuery, setOptimisticQuery] = useOptimistic(
routeSearchParams.toString(),
(_current, next: string) => next,
);
const active = new URLSearchParams(optimisticQuery).get("provider") === "8";
return (
{
const params = new URLSearchParams(optimisticQuery);
if (active) params.delete("provider");
else params.set("provider", "8");
const query = params.toString();
startTransition(() => {
setOptimisticQuery(query);
router.push(query ? `/top?${query}` : "/top", { scroll: false });
});
}}
>
Netflix
);
}
```
## Steps
1. Load `/top`.
2. Click the provider toggle. The navigation to `/top?provider=8` commits and filtered server content renders.
3. Click the active provider toggle again. This calls `router.push("/top", { scroll: false })`.
4. Observe the transition.
## Actual behavior
The request sequence includes a successful `GET /top?_rsc=...` response, but the navigation can fail to commit on the client. The provider remains active / the filtered tree remains mounted. During the same production session, the client also reached vinext's default `This page couldn’t load` boundary without a server digest.
## Expected behavior
The transition commits to `/top`, `useSearchParams()` becomes empty for `provider`, and the unfiltered server tree replaces the filtered tree.
## Notes
- This was observed in a real Cloudflare Workers deployment immediately after migrating an existing Next.js App Router application to vinext.
- A fresh isolated browser did not reproduce it every time, so a cache or in-flight navigation race may be involved.
- The server-side RSC request succeeded, which points to client navigation/cache commit rather than the route's data fetch.
- #1621 describes a closely related stale-search-param cache-key failure, so this may be a regression or an uncovered transition path.
## Environment
- vinext: 1.0.0-beta.5
- Next.js: 16.2.10
- React / React DOM: 19.2.8
- Runtime: Cloudflare Workers
Contributor guide
Research direction
Start by reproducing the supplied ProviderToggle flow: navigate from `/top` to `/top?provider=8`, then remove the last query parameter; compare the successful RSC response with the client’s committed route state. Read the related issue #1621 and trace the client navigation/cache commit path. Done means the transition reliably commits to `/top`, clears the provider search parameter, and renders the unfiltered server tree.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- next.js, react, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100