diff --git a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js
index 3c1012518683..b24741477866 100644
--- a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js
+++ b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js
@@ -2169,6 +2169,29 @@ describe('ReactInternalTestUtils console assertions', () => {
+ Bye in div (at **)"
`);
});
+
+ // @gate __DEV__
+ it('fails if last received error containing "undefined" is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi');
+ console.error(
+ "TypeError: Cannot read properties of undefined (reading 'stack')\n" +
+ ' in Foo (at **)'
+ );
+ assertConsoleErrorDev([['Hi', {withoutStack: true}]]);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Unexpected error(s) recorded.
+
+ - Expected errors
+ + Received errors
+
+ Hi
+ + TypeError: Cannot read properties of undefined (reading 'stack') in Foo (at **)"
+ `);
+ });
// @gate __DEV__
it('fails if only error does not contain a stack', () => {
const message = expectToThrowFailure(() => {
diff --git a/packages/internal-test-utils/consoleMock.js b/packages/internal-test-utils/consoleMock.js
index ecb97b3a0305..743519590e37 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
@@ -382,8 +382,9 @@ export function createLogAssertion(
// Main logic to check if log is expected, with the component stack.
if (
- normalizedMessage === expectedMessage ||
- normalizedMessage.includes(expectedMessage)
+ typeof expectedMessage === 'string' &&
+ (normalizedMessage === expectedMessage ||
+ normalizedMessage.includes(expectedMessage))
) {
if (isLikelyAComponentStack(normalizedMessage)) {
if (expectedWithoutStack === true) {
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
index 7b19908cc8c4..fd068c0ad185 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
@@ -24,7 +24,6 @@ import FetchFileWithCachingContext from './FetchFileWithCachingContext';
import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource';
import OpenInEditorButton from './OpenInEditorButton';
import InspectedElementViewSourceButton from './InspectedElementViewSourceButton';
-import Skeleton from './Skeleton';
import useEditorURL from '../useEditorURL';
import styles from './InspectedElement.css';
@@ -203,7 +202,9 @@ export default function InspectedElementWrapper(_: Props): React.Node {
}
return (
-
+
{strictModeBadge}
@@ -232,13 +233,11 @@ export default function InspectedElementWrapper(_: Props): React.Node {
!!editorURL &&
source != null &&
symbolicatedSourcePromise != null && (
-
}>
-
-
+
)}
{canToggleError && (
@@ -294,9 +293,6 @@ export default function InspectedElementWrapper(_: Props): React.Node {
{inspectedElement !== null && symbolicatedSourcePromise != null && (
source
- }>
+
+
+
+ }>
rendered by
-
- {showStack ? : null}
- {showOwnersList &&
- owners?.map(owner => (
-
-
- {owner.stack != null && owner.stack.length > 0 ? (
-
- ) : null}
-
- ))}
-
- {rootType !== null && (
- {rootType}
- )}
- {rendererLabel !== null && (
- {rendererLabel}
- )}
+
+
+
+ }>
+ {showStack ?
: null}
+ {showOwnersList &&
+ owners?.map(owner => (
+
+
+ {owner.stack != null && owner.stack.length > 0 ? (
+
+ ) : null}
+
+ ))}
+
+ {rootType !== null && (
+
{rootType}
+ )}
+ {rendererLabel !== null && (
+
{rendererLabel}
+ )}
+
)}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementViewSourceButton.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementViewSourceButton.js
index 23d4cf96c827..ee2fbe6c4d20 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementViewSourceButton.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementViewSourceButton.js
@@ -11,7 +11,6 @@ import * as React from 'react';
import ButtonIcon from '../ButtonIcon';
import Button from '../Button';
-import Skeleton from './Skeleton';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
@@ -27,7 +26,12 @@ function InspectedElementViewSourceButton({
symbolicatedSourcePromise,
}: Props): React.Node {
return (
-
}>
+
+
+
+ }>
+
+ Loading source maps...
+
+ }>
+
+
+ );
+}
+
export default OpenInEditorButton;
diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js
index 19a49dcd32b9..bc9c259b3922 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 b81a793a7f29..c924a52c4f41 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')",
+ ]);
+ });
});