Skip to content

fix(app-router): honor reactStrictMode#2679

Open
NathanDrake2406 wants to merge 2 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/fix-app-router-strict-mode
Open

fix(app-router): honor reactStrictMode#2679
NathanDrake2406 wants to merge 2 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/fix-app-router-strict-mode

Conversation

@NathanDrake2406

@NathanDrake2406 NathanDrake2406 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #2676.

Summary

goal core change primary files expected impact
Match the Next.js App Router reactStrictMode contract Publish __NEXT_STRICT_MODE_APP from resolved config and wrap the shared hydration/CSR root with StrictMode or Fragment src/index.ts, server/app-browser-entry.ts App Router strict checks run by default in development; reactStrictMode: false remains an opt-out

Why

Next.js enables Strict Mode by default for the App Router and preserves an explicit reactStrictMode: false. vinext already preserved the resolved boolean | null value, but the App Router build did not publish that value to its browser entry and the browser root was never wrapped. As a result, the App Router ignored both the default and explicit true configuration.

This follows Next.js's implementation: resolve the App Router default in the build define, select StrictMode or Fragment once, and use that wrapper for both hydration and the CSR error fallback. I found no dedicated Next.js behavioral test for this config; the pinned build and client sources below are the executable contract.

What changed

scenario before after
App Router, config unset No Strict Mode wrapper Strict Mode enabled
App Router, reactStrictMode: true No Strict Mode wrapper Strict Mode enabled
App Router, reactStrictMode: false No Strict Mode wrapper Fragment used; Strict Mode disabled
Pages Router Existing behavior Unchanged
vinext check Reports reactStrictMode as partial Reports it as supported

The browser regressions count client renders without scheduling a React state update. The default fixture expects two development renders; an isolated fixture with reactStrictMode: false expects one. The latter exercises the complete path from next.config.ts through Vite's define replacement and the browser root wrapper.

Validation

  • Reproduced the original behavior: the App Router fixture rendered once when the test expected the default Strict Mode count of two.
  • Verified the default regression fails when the completed root wrapper is forced to Fragment: expected 2, received 1.
  • Verified the opt-out regression fails when the browser entry treats raw boolean false as though it were the string "false": expected 1, received 2.
  • Verified config define output for unset, explicit true, and explicit false.
  • Ran 381 targeted unit tests across the config, compatibility scanner, and App Router browser entry.
  • Ran both App Router Playwright regressions: 2 passed.
  • Built the vinext package.
  • Ran scoped formatting, lint, and type checks. The pre-commit full check and knip check also passed.
Commands
vp check packages/vinext/src/check.ts packages/vinext/src/index.ts packages/vinext/src/server/app-browser-entry.ts tests/check.test.ts tests/react-strict-mode.test.ts tests/e2e/app-router/react-strict-mode.spec.ts tests/fixtures/app-basic/app/react-strict-mode/render-probe.tsx tests/fixtures/app-basic/app/react-strict-mode/page.tsx
vp check tests/e2e/app-router/react-strict-mode.spec.ts tests/fixtures/app-basic/react-strict-mode-disabled/app/layout.tsx tests/fixtures/app-basic/react-strict-mode-disabled/app/page.tsx tests/fixtures/app-basic/react-strict-mode-disabled/next.config.ts tests/fixtures/app-basic/react-strict-mode-disabled/vite.config.ts
vp test run tests/react-strict-mode.test.ts tests/check.test.ts tests/app-browser-entry.test.ts
vp run vinext#build
PLAYWRIGHT_PROJECT=app-router npx playwright test tests/e2e/app-router/react-strict-mode.spec.ts --retries=0

Risk

Low. The change is confined to the App Router client root and its build-time boolean. React Strict Mode's additional checks are development-only, while an explicit false selects Fragment. Existing Pages Router behavior and defaults are unchanged. App Router applications may now expose non-idempotent development behavior that Strict Mode is intended to detect.

References

reference why it matters
Issue #2676 Bug report and affected configuration
Prior Pages Router work in #2433 Deliberately left the App Router root out of scope
Next.js build define Defaults App Router Strict Mode on and preserves explicit false
Next.js client root Selects StrictMode or Fragment
Next.js hydration and CSR wrapper Shares the wrapped tree between both root paths
Next.js config documentation Documents the App Router default and explicit opt-out
Browser regressions Exercise default-on and explicit-false behavior after hydration
Explicit-false config Drives the full opt-out path from next.config.ts
Opt-out render probe Observes one render without scheduling another
Config regression Covers unset, enabled, and disabled define values

App Router client roots were never wrapped in React StrictMode, so the router ignored both Next.js default-on behavior and explicit configuration.

Publish the resolved App Router strict-mode define and select StrictMode or Fragment around the shared hydration and CSR root. This preserves the Pages Router default while matching Next.js for App Router applications.

Add config and browser regression coverage for default, enabled, and disabled values.
@pkg-pr-new

pkg-pr-new Bot commented Jul 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2679
npm i https://pkg.pr.new/create-vinext-app@2679
npm i https://pkg.pr.new/@vinext/types@2679
npm i https://pkg.pr.new/vinext@2679

commit: 7db6b7b

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 7db6b7b against base 22f106e using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 131.9 KB 132.0 KB ⚫ +0.0%
Client entry size (gzip) vinext 119.3 KB 119.3 KB ⚫ +0.0%
Dev server cold start vinext 2.80 s 2.81 s ⚫ +0.6%
Production build time vinext 3.07 s 3.05 s ⚫ -0.4%
RSC entry closure size (gzip) vinext 103.2 KB 103.2 KB ⚫ -0.0%
Server bundle size (gzip) vinext 177.5 KB 177.5 KB ⚫ -0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

The App Router browser coverage only exercised the default-enabled path, so a boolean/string mismatch could enable Strict Mode even when the emitted define was false.

Run an isolated development fixture with reactStrictMode set to false and assert a single client render. The fixture keeps its optimizer cache and server lifecycle separate from the default-enabled app.
@NathanDrake2406
NathanDrake2406 marked this pull request as ready for review July 23, 2026 07:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

App Router ignores reactStrictMode in next.config (Pages Router only today)

1 participant