next/dynamic's ssr:false server branch rendered the loading component with
pastDelay:false, while the client's first (pre-mount) render and Suspense
fallback default pastDelay to true. For a loading component that branches on
pastDelay (the documented `if (!pastDelay) return null` pattern), the server
emitted nothing but the client rendered the spinner, causing a React hydration
mismatch and a visible content jump.
Drop the hardcoded pastDelay:false override on the server so it defaults to
true, matching both the client render and Next.js App Router, which always
renders the loading fallback with pastDelay=true on server and client. Loading
components that ignore pastDelay are unaffected.
Update the three tests that asserted the buggy server-side pastDelay:false, and
add a server-equals-client-first-render parity assertion.
Fixes#1967
dynamic() currently renders the ssr:false server loading component with pastDelay=true. That disagrees with Next.js noSSR behavior and makes loading components that suppress UI before pastDelay render the opposite state during SSR.
The ssr:false server placeholder now passes pastDelay=false while the existing SSR-enabled and client Suspense fallback paths keep their current loading props. Tests cover both the raw prop contract and the observable null-until-delay loading behavior.
vinext previously treated the first next/dynamic argument as a callable loader. That broke valid Next.js call shapes such as a direct import promise and an options object with loader because React.lazy eventually tried to call a non-function value.
The shim now normalizes the public dynamic input before rendering: promises become loader functions, function loaders stay unchanged, and options objects merge with the second options argument. Focused tests cover the legacy promise form and the options-object loader form against SSR output.
vinext only passed isLoading, pastDelay, and error to next/dynamic loading components. Next.js includes retry and timedOut, so loading UIs that destructure those props observed an incompatible shape. The shim assumed the loading prop contract was limited to the values it currently used.
This aligns the runtime and ambient shim declarations with Next.js by passing the full loading props shape, preserving current delay defaults, and wiring client retry through a replaceable lazy component. The regression test covers the SSR loading object shape.
* fix: RSC compatibility for dynamic() and layout segment context
Three related fixes for React Server Component environments:
1. **dynamic.ts: Remove "use client" and add RSC async path**
The `"use client"` directive forced `next/dynamic` into a client
component boundary, but `dynamic()` should work in server components
too. In the RSC environment, `React.lazy` is not available (the
`react-server` condition exports a stripped-down React). Added a
runtime check: when `React.lazy` is not a function, use an async
server component pattern instead (the RSC renderer natively supports
async components).
Also switched from destructured imports (`lazy`, `Suspense`,
`useState`, `useEffect`) to `React.lazy`, `React.Suspense`, etc.
to avoid importing names that don't exist under the `react-server`
condition.
2. **layout-segment-context.tsx: Remove "use client"**
This module is imported directly by the RSC entry. The `"use client"`
directive created a client component boundary that breaks the RSC
rendering pipeline. `getLayoutSegmentContext()` already returns
`null` when `React.createContext` is unavailable (RSC), and the
`LayoutSegmentProvider` gracefully falls back to passing children
through unchanged.
3. **app-rsc-entry.ts: Wrap route handler params with makeThenableParams**
Next.js 15+ changed route handler params to be async (Promises).
Route handlers that `await params` crash when params is a plain
object. `makeThenableParams()` wraps the object so it's both a
Promise and has synchronous property access.
* chore: fix formatting and update entry-templates snapshots after merge with main
* fix: restore 'use client' in layout-segment-context to fix useSelectedLayoutSegment(s)
Removing 'use client' caused LayoutSegmentProvider to run in the RSC
environment where React.createContext is undefined. getLayoutSegmentContext()
returned null, the provider became a no-op, and useSelectedLayoutSegments
always returned [] instead of the actual segments.
The 'use client' boundary is required so the RSC bundler renders this
component in the SSR/browser environment where createContext works.
* test: add regression tests for RSC dynamic() and route handler await params
Add two regression tests for the fixes in PR #466:
1. Route handler await params (Next.js 15 async params pattern):
Tests /api/catch-all/[...slugs] which uses `await params` in the
handler — verifies that makeThenableParams() correctly wraps params
so both await and direct property access work.
2. dynamic() in RSC (async component path):
Adds a pure server component fixture that uses dynamic() without
"use client". In the RSC environment React.lazy is unavailable, so
dynamic() must fall back to the async component pattern. Tests that
the dynamically-loaded component renders in the HTML output.
* fix: address bonk review comments on dynamic.ts RSC async path
- Add comments explaining LoadingComponent is intentionally ignored in
the RSC async fallback path (parent Suspense boundaries handle loading
states; error handling defers to nearest error boundary)
- Add comment explaining the 'as unknown as ComponentType<P>' cast is
safe because the RSC renderer natively supports async components, but
TypeScript's ComponentType<P> doesn't account for async return types
- Add unit tests for the AsyncServerDynamic path by stubbing React.lazy
to undefined (simulating a react-server condition without lazy).
Verifies: displayName, async resolution, bare component exports,
and that LoadingComponent is correctly ignored.
Note: React 19.x exports React.lazy from the react-server condition,
so typeof React.lazy !== 'function' does not trigger in current React.
The AsyncServerDynamic path is defensive forward-compatibility code.
The unit tests stub React.lazy to exercise it directly.
* chore: remove unused vi import from dynamic.test.ts
* docs: correct inaccurate React.lazy/RSC comments throughout
React 19.x exports React.lazy from the react-server condition, so the
typeof React.lazy !== 'function' guard does not trigger in current
React. The AsyncServerDynamic path is defensive forward-compatibility
code only — it does not execute today.
Update all comments that incorrectly claimed React.lazy is unavailable
in the react-server environment:
- dynamic.ts: top-level doc comment and guard comment
- dynamic-rsc.tsx fixture: comment explaining the RSC path
- rsc-dynamic/page.tsx fixture: explain which path actually executes
- nextjs-compat/dynamic.test.ts: clarify that the integration test
exercises LazyServer + Suspense, not AsyncServerDynamic
---------
Co-authored-by: James <james@eli.cx>
* chore: migrate to vite plus
* Disable typeAware and typeCheck
* Update CI
* Fix CI
* Fix test
* Clean
* Run test with vp
* Try revert
* react: false In test
* Fix test
* Revert "Try revert"
This reverts commit 009da10473.
* Update
* Update
* Try revert ci changes
* revert
* Run vp migrate
* Disable typeAware and typeCheck for now
* Better resolve for test
* Use vp dev instead of vite
* Update expect
* Fix NormalizeManifestModuleId
* Try increase timeout
* Update to use vp
* Try new check
* Bring back npx vp
* Migrate CI
* Make next-intl resolvable
* Update
* Update
* Update
* add oxfmt formatter: config, scripts, CI, editor setup, docs
* rebuild lockfile
* fix: add Format to required checks list, remove dead ignore pattern
* run fmt
* add format to agents.md again