diff --git a/packages/internal-test-utils/consoleMock.js b/packages/internal-test-utils/consoleMock.js index ecb97b3a03059..989bb82f22edd 100644 --- a/packages/internal-test-utils/consoleMock.js +++ b/packages/internal-test-utils/consoleMock.js @@ -355,7 +355,7 @@ export function createLogAssertion( let argIndex = 0; // console.* could have been called with a non-string e.g. `console.error(new Error())` // eslint-disable-next-line react-internal/safe-string-coercion - String(format).replace(/%s|%c/g, () => argIndex++); + String(format).replace(/%s|%c|%o/g, () => argIndex++); if (argIndex !== args.length) { if (format.includes('%c%s')) { // We intentionally use mismatching formatting when printing badging because we don't know diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 19a49dcd32b98..bc9c259b3922f 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -3354,6 +3354,27 @@ function renderModelDestructive( task.debugOwner = element._owner; task.debugStack = element._debugStack; task.debugTask = element._debugTask; + if ( + element._owner === undefined || + element._debugStack === undefined || + element._debugTask === undefined + ) { + let key = ''; + if (element.key !== null) { + key = ' key="' + element.key + '"'; + } + + console.error( + 'Attempted to render <%s%s> without development properties. ' + + 'This is not supported. It can happen if:' + + '\n- The element is created with a production version of React but rendered in development.' + + '\n- The element was cloned with a custom function instead of `React.cloneElement`.\n' + + 'The props of this element may help locate this element: %o', + element.type, + key, + element.props, + ); + } // TODO: Pop this. Since we currently don't have a point where we can pop the stack // this debug information will be used for errors inside sibling properties that // are not elements. Leading to the wrong attribution on the server. We could fix diff --git a/packages/react-server/src/__tests__/ReactFlightServer-test.js b/packages/react-server/src/__tests__/ReactFlightServer-test.js index b81a793a7f29d..c924a52c4f417 100644 --- a/packages/react-server/src/__tests__/ReactFlightServer-test.js +++ b/packages/react-server/src/__tests__/ReactFlightServer-test.js @@ -36,6 +36,7 @@ let ReactNoopFlightServer; let Scheduler; let advanceTimersByTime; let assertLog; +let assertConsoleErrorDev; describe('ReactFlight', () => { beforeEach(() => { @@ -64,6 +65,7 @@ describe('ReactFlight', () => { Scheduler = require('scheduler'); const InternalTestUtils = require('internal-test-utils'); assertLog = InternalTestUtils.assertLog; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; }); afterEach(() => { @@ -175,4 +177,26 @@ describe('ReactFlight', () => { stackTwo: '\n in OwnerStackDelayed (at **)' + '\n in App (at **)', }); }); + + it('logs an error when prod elements are rendered', async () => { + const element = ReactServer.createElement('span', { + key: 'one', + children: 'Free!', + }); + ReactNoopFlightServer.render( + // bad clone + {...element}, + ); + + assertConsoleErrorDev([ + [ + 'Attempted to render without development properties. This is not supported. It can happen if:' + + '\n- The element is created with a production version of React but rendered in development.' + + '\n- The element was cloned with a custom function instead of `React.cloneElement`.\n' + + "The props of this element may help locate this element: { children: 'Free!', [key]: [Getter] }", + {withoutStack: true}, + ], + "TypeError: Cannot read properties of undefined (reading 'stack')", + ]); + }); });