next.js
45d26dfd - Fix dev overlay hydration error ordering (#94555)

Commit
11 days ago
Fix dev overlay hydration error ordering (#94555) ### What? Preserve FIFO ordering for events received while the dev overlay connects its React dispatcher. - Replace the separate global dispatch pointer and backlog with an `EventQueue`. - Keep new events queued until the existing backlog has finished replaying. - Add focused unit coverage for normal ordering, events enqueued during replay, and replay failures. ### Why? The React 18 Pages Router hydration test for an extra node inside Suspense reports three events in this order: 1. The runtime hydration mismatch captured from the error/console path. 2. The recoverable hydration mismatch. 3. The recoverable Suspense boundary client-render fallback. The overlay previously set `maybeDispatch` from `useInsertionEffect`, but deferred replaying its existing queue with `setTimeout`. If the first event arrived before the overlay connected, then the remaining recoverable events arrived after `maybeDispatch` was exposed but before the timeout, those newer events dispatched immediately and overtook the queued event. The resulting order was 2, 3, 1, which caused the inline snapshot failure. Investigation also found that enabling Node.js streams was correlated with the failure but was not its direct cause: - The failing test uses the Pages Router renderer, which still calls `ReactDOMServerPages.renderToReadableStream` and does not select the App Router Node Fizz stream implementation. - The Node streams default change did not modify the Pages client error bridge or dev overlay queue. - The queue race predates the Node streams rollout. Timing changes around the rollout, including adjacent lazy loading of the overlay UX, made the existing race reproducible. The correct fix is therefore to enforce the overlay queue ordering invariant rather than update the snapshot or special-case Node streams. ### How? `EventQueue.connect()` leaves direct dispatch disabled while draining the backlog. Events received during replay are appended to the same queue and drained before the dispatcher becomes directly available. A `finally` block preserves the previous cleanup and connection behavior when a queued event throws, while `disconnect()` only clears the currently connected dispatcher. ### Verification - `pnpm prettier --with-node-modules --ignore-path .prettierignore --write packages/next/src/next-devtools/dev-overlay.browser.tsx packages/next/src/next-devtools/dev-overlay/event-queue.ts packages/next/src/next-devtools/dev-overlay/event-queue.test.ts` - `npx eslint --config eslint.config.mjs --fix packages/next/src/next-devtools/dev-overlay.browser.tsx packages/next/src/next-devtools/dev-overlay/event-queue.ts packages/next/src/next-devtools/dev-overlay/event-queue.test.ts` - `pnpm --filter=next build` - `pnpm exec jest packages/next/src/next-devtools/dev-overlay/event-queue.test.ts --runInBand` - `HEADLESS=true NEXT_TEST_REACT_VERSION=18.3.1 NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-webpack test/development/acceptance/hydration-error.test.ts -t "should show correct hydration error when client renders an extra node inside Suspense content"` - `HEADLESS=true NEXT_TEST_REACT_VERSION=18.3.1 NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-turbo test/development/acceptance/hydration-error.test.ts -t "should show correct hydration error when client renders an extra node inside Suspense content"` <!-- NEXT_JS_LLM_PR -->
Author
Parents
Loading