The issue involves a specific interaction between Flight Client and debugChannel that affects element validation.
The issue occurs when `debugChannel` is enabled in React Flight Client, causing elements to be wrapped in lazy chunks and `isValidElement()` to return false. This affects hydration and element validation. The fix requires understanding how Flight Client handles debug data and element deserialization.
React version: 19.3.0-canary (tested from f93b9fd4-20251217 through b4546cd0-20260318)
git clone https://github.com/Gyeonghun-Park/react-flight-debugchannel-repro
cd react-flight-debugchannel-repro
npm install
npm test
React 19.3.0-canary-f93b9fd4-20251217 (NODE_ENV=development)
Without debugChannel:
isValidElement: true
$$typeof: Symbol(react.transitional.element)
With debugChannel (debug data delayed 50 ms):
isValidElement: false
$$typeof: Symbol(react.lazy)
✗ Bug reproduced: Flight Client returned a lazy wrapper
instead of a React element when debugChannel is enabled.
Link to code example: https://github.com/Gyeonghun-Park/react-flight-debugchannel-repro
The test renders <ClientComponent><div>hello</div></ClientComponent> via Flight Server, deserializes it via Flight Client, then checks isValidElement() on the result. A 50ms delay on the debug channel stream simulates real-world conditions where debug info is delivered via a separate transport (e.g. WebSocket).
This issue was originally discovered via Next.js 16.2.0, which recently changed the default of experimental.reactDebugChannel from false to true. A visual reproduction with browser hydration is available at:
https://github.com/Gyeonghun-Park/next-isvalidelement-repro
Setting experimental: { reactDebugChannel: false } in next.config.ts immediately resolves the issue, which is what led us to identify debugChannel as the trigger.
When debugChannel is passed to createFromNodeStream (or createFromReadableStream) and the debug data arrives after the element data, deserialized React elements are wrapped in createLazyChunkWrapper with $$typeof: Symbol(react.lazy). As a result:
React.isValidElement() returns falseReact.cloneElement() throwsClaim this issue to let others know you're working on it. You'll earn 20 points when you complete it!