@samitouri / QOS-React-2 / commits / bc358362a6

[Flight] Improve Error Messages when Invalid Object is Passed to Client/Host Components (#25492)

* Print built-in specific error message for toJSON This is a better message for Date. Also, format the message to highlight the affected prop. * Describe error messages using JSX elements in DEV We don't have access to the grand parent objects on the stack so we stash them on weakmaps so we can access them while printing error messages. Might be a bit slow. * Capitalize Server/Client Component * Special case errror messages for children of host components These are likely meant to be text content if they're not a supported object. * Update error messages

Sebastian Markbåge committed Oct 16, 2022 at 21:49 UTC bc358362a6dc9265705f9f09cba78bb919254737
6 files changed +445 -150
packages/react-client/src/__tests__/ReactFlight-test.js
+203 -28
@@ -95,7 +95,7 @@ describe('ReactFlight', () => {
95 };
96 }
97
98 - it('can render a server component', async () => {
98 + it('can render a Server Component', async () => {
99 function Bar({text}) {
100 return text.toUpperCase();
101 }
@@ -125,7 +125,7 @@ describe('ReactFlight', () => {
125 });
126 });
127
128 - it('can render a client component using a module reference and render there', async () => {
128 + it('can render a Client Component using a module reference and render there', async () => {
129 function UserClient(props) {
130 return (
131 <span>
@@ -363,6 +363,11 @@ describe('ReactFlight', () => {
363
364 // @gate enableUseHook
365 it('should error if a non-serializable value is passed to a host component', async () => {
366 + function ClientImpl({children}) {
367 + return children;
368 + }
369 + const Client = moduleReference(ClientImpl);
370 +
371 function EventHandlerProp() {
372 return (
373 <div className="foo" onClick={function() {}}>
@@ -382,6 +387,24 @@ describe('ReactFlight', () => {
387 return <div ref={ref} />;
388 }
389
390 + function EventHandlerPropClient() {
391 + return (
392 + <Client className="foo" onClick={function() {}}>
393 + Test
394 + </Client>
395 + );
396 + }
397 + function FunctionPropClient() {
398 + return <Client>{() => {}}</Client>;
399 + }
400 + function SymbolPropClient() {
401 + return <Client foo={Symbol('foo')} />;
402 + }
403 +
404 + function RefPropClient() {
405 + return <Client ref={ref} />;
406 + }
407 +
408 const options = {
409 onError(x) {
410 return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
@@ -391,8 +414,21 @@ describe('ReactFlight', () => {
414 const fn = ReactNoopFlightServer.render(<FunctionProp />, options);
415 const symbol = ReactNoopFlightServer.render(<SymbolProp />, options);
416 const refs = ReactNoopFlightServer.render(<RefProp />, options);
417 + const eventClient = ReactNoopFlightServer.render(
418 + <EventHandlerPropClient />,
419 + options,
420 + );
421 + const fnClient = ReactNoopFlightServer.render(
422 + <FunctionPropClient />,
423 + options,
424 + );
425 + const symbolClient = ReactNoopFlightServer.render(
426 + <SymbolPropClient />,
427 + options,
428 + );
429 + const refsClient = ReactNoopFlightServer.render(<RefPropClient />, options);
430
395 - function Client({promise}) {
431 + function Render({promise}) {
432 return use(promise);
433 }
434
@@ -400,17 +436,29 @@ describe('ReactFlight', () => {
436 startTransition(() => {
437 ReactNoop.render(
438 <>
403 - <ErrorBoundary expectedMessage="Event handlers cannot be passed to client component props.">
404 - <Client promise={ReactNoopFlightClient.read(event)} />
439 + <ErrorBoundary expectedMessage="Event handlers cannot be passed to Client Component props.">
440 + <Render promise={ReactNoopFlightClient.read(event)} />
441 + </ErrorBoundary>
442 + <ErrorBoundary expectedMessage="Functions cannot be passed directly to Client Components because they're not serializable.">
443 + <Render promise={ReactNoopFlightClient.read(fn)} />
444 + </ErrorBoundary>
445 + <ErrorBoundary expectedMessage="Only global symbols received from Symbol.for(...) can be passed to Client Components.">
446 + <Render promise={ReactNoopFlightClient.read(symbol)} />
447 + </ErrorBoundary>
448 + <ErrorBoundary expectedMessage="Refs cannot be used in Server Components, nor passed to Client Components.">
449 + <Render promise={ReactNoopFlightClient.read(refs)} />
450 + </ErrorBoundary>
451 + <ErrorBoundary expectedMessage="Event handlers cannot be passed to Client Component props.">
452 + <Render promise={ReactNoopFlightClient.read(eventClient)} />
453 </ErrorBoundary>
406 - <ErrorBoundary expectedMessage="Functions cannot be passed directly to client components because they're not serializable.">
407 - <Client promise={ReactNoopFlightClient.read(fn)} />
454 + <ErrorBoundary expectedMessage="Functions cannot be passed directly to Client Components because they're not serializable.">
455 + <Render promise={ReactNoopFlightClient.read(fnClient)} />
456 </ErrorBoundary>
409 - <ErrorBoundary expectedMessage="Only global symbols received from Symbol.for(...) can be passed to client components.">
410 - <Client promise={ReactNoopFlightClient.read(symbol)} />
457 + <ErrorBoundary expectedMessage="Only global symbols received from Symbol.for(...) can be passed to Client Components.">
458 + <Render promise={ReactNoopFlightClient.read(symbolClient)} />
459 </ErrorBoundary>
412 - <ErrorBoundary expectedMessage="Refs cannot be used in server components, nor passed to client components.">
413 - <Client promise={ReactNoopFlightClient.read(refs)} />
460 + <ErrorBoundary expectedMessage="Refs cannot be used in Server Components, nor passed to Client Components.">
461 + <Render promise={ReactNoopFlightClient.read(refsClient)} />
462 </ErrorBoundary>
463 </>,
464 );
@@ -419,19 +467,19 @@ describe('ReactFlight', () => {
467 });
468
469 // @gate enableUseHook
422 - it('should trigger the inner most error boundary inside a client component', async () => {
470 + it('should trigger the inner most error boundary inside a Client Component', async () => {
471 function ServerComponent() {
424 - throw new Error('This was thrown in the server component.');
472 + throw new Error('This was thrown in the Server Component.');
473 }
474
475 function ClientComponent({children}) {
428 - // This should catch the error thrown by the server component, even though it has already happened.
476 + // This should catch the error thrown by the Server Component, even though it has already happened.
477 // We currently need to wrap it in a div because as it's set up right now, a lazy reference will
478 // throw during reconciliation which will trigger the parent of the error boundary.
479 // This is similar to how these will suspend the parent if it's a direct child of a Suspense boundary.
480 // That's a bug.
481 return (
434 - <ErrorBoundary expectedMessage="This was thrown in the server component.">
482 + <ErrorBoundary expectedMessage="This was thrown in the Server Component.">
483 <div>{children}</div>
484 </ErrorBoundary>
485 );
@@ -475,25 +523,37 @@ describe('ReactFlight', () => {
523 );
524 ReactNoopFlightClient.read(transport);
525 }).toErrorDev(
478 - 'Only plain objects can be passed to client components from server components. ',
526 + 'Only plain objects can be passed to Client Components from Server Components. ' +
527 + 'Date objects are not supported.',
528 {withoutStack: true},
529 );
530 });
531
483 - it('should warn in DEV if a special object is passed to a host component', () => {
532 + it('should warn in DEV if a toJSON instance is passed to a host component child', () => {
533 expect(() => {
485 - const transport = ReactNoopFlightServer.render(<input value={Math} />);
534 + const transport = ReactNoopFlightServer.render(
535 + <div>Current date: {new Date()}</div>,
536 + );
537 ReactNoopFlightClient.read(transport);
538 }).toErrorDev(
488 - 'Only plain objects can be passed to client components from server components. ' +
489 - 'Built-ins like Math are not supported.',
539 + 'Date objects cannot be rendered as text children. Try formatting it using toString().\n' +
540 + ' <div>Current date: {Date}</div>\n' +
541 + ' ^^^^^^',
542 {withoutStack: true},
543 );
544 });
545
494 - it('should NOT warn in DEV for key getters', () => {
495 - const transport = ReactNoopFlightServer.render(<div key="a" />);
496 - ReactNoopFlightClient.read(transport);
546 + it('should warn in DEV if a special object is passed to a host component', () => {
547 + expect(() => {
548 + const transport = ReactNoopFlightServer.render(<input value={Math} />);
549 + ReactNoopFlightClient.read(transport);
550 + }).toErrorDev(
551 + 'Only plain objects can be passed to Client Components from Server Components. ' +
552 + 'Math objects are not supported.\n' +
553 + ' <input value={Math}>\n' +
554 + ' ^^^^^^',
555 + {withoutStack: true},
556 + );
557 });
558
559 it('should warn in DEV if an object with symbols is passed to a host component', () => {
@@ -503,12 +563,127 @@ describe('ReactFlight', () => {
563 );
564 ReactNoopFlightClient.read(transport);
565 }).toErrorDev(
506 - 'Only plain objects can be passed to client components from server components. ' +
566 + 'Only plain objects can be passed to Client Components from Server Components. ' +
567 'Objects with symbol properties like Symbol.iterator are not supported.',
568 {withoutStack: true},
569 );
570 });
571
572 + it('should warn in DEV if a toJSON instance is passed to a Client Component', () => {
573 + function ClientImpl({value}) {
574 + return <div>{value}</div>;
575 + }
576 + const Client = moduleReference(ClientImpl);
577 + expect(() => {
578 + const transport = ReactNoopFlightServer.render(
579 + <Client value={new Date()} />,
580 + );
581 + ReactNoopFlightClient.read(transport);
582 + }).toErrorDev(
583 + 'Only plain objects can be passed to Client Components from Server Components. ' +
584 + 'Date objects are not supported.',
585 + {withoutStack: true},
586 + );
587 + });
588 +
589 + it('should warn in DEV if a toJSON instance is passed to a Client Component child', () => {
590 + function ClientImpl({children}) {
591 + return <div>{children}</div>;
592 + }
593 + const Client = moduleReference(ClientImpl);
594 + expect(() => {
595 + const transport = ReactNoopFlightServer.render(
596 + <Client>Current date: {new Date()}</Client>,
597 + );
598 + ReactNoopFlightClient.read(transport);
599 + }).toErrorDev(
600 + 'Only plain objects can be passed to Client Components from Server Components. ' +
601 + 'Date objects are not supported.\n' +
602 + ' <>Current date: {Date}</>\n' +
603 + ' ^^^^^^',
604 + {withoutStack: true},
605 + );
606 + });
607 +
608 + it('should warn in DEV if a special object is passed to a Client Component', () => {
609 + function ClientImpl({value}) {
610 + return <div>{value}</div>;
611 + }
612 + const Client = moduleReference(ClientImpl);
613 + expect(() => {
614 + const transport = ReactNoopFlightServer.render(<Client value={Math} />);
615 + ReactNoopFlightClient.read(transport);
616 + }).toErrorDev(
617 + 'Only plain objects can be passed to Client Components from Server Components. ' +
618 + 'Math objects are not supported.\n' +
619 + ' <... value={Math}>\n' +
620 + ' ^^^^^^',
621 + {withoutStack: true},
622 + );
623 + });
624 +
625 + it('should warn in DEV if an object with symbols is passed to a Client Component', () => {
626 + function ClientImpl({value}) {
627 + return <div>{value}</div>;
628 + }
629 + const Client = moduleReference(ClientImpl);
630 + expect(() => {
631 + const transport = ReactNoopFlightServer.render(
632 + <Client value={{[Symbol.iterator]: {}}} />,
633 + );
634 + ReactNoopFlightClient.read(transport);
635 + }).toErrorDev(
636 + 'Only plain objects can be passed to Client Components from Server Components. ' +
637 + 'Objects with symbol properties like Symbol.iterator are not supported.',
638 + {withoutStack: true},
639 + );
640 + });
641 +
642 + it('should warn in DEV if a special object is passed to a nested object in Client Component', () => {
643 + function ClientImpl({value}) {
644 + return <div>{value}</div>;
645 + }
646 + const Client = moduleReference(ClientImpl);
647 + expect(() => {
648 + const transport = ReactNoopFlightServer.render(
649 + <Client value={{hello: Math, title: <h1>hi</h1>}} />,
650 + );
651 + ReactNoopFlightClient.read(transport);
652 + }).toErrorDev(
653 + 'Only plain objects can be passed to Client Components from Server Components. ' +
654 + 'Math objects are not supported.\n' +
655 + ' {hello: Math, title: <h1/>}\n' +
656 + ' ^^^^',
657 + {withoutStack: true},
658 + );
659 + });
660 +
661 + it('should warn in DEV if a special object is passed to a nested array in Client Component', () => {
662 + function ClientImpl({value}) {
663 + return <div>{value}</div>;
664 + }
665 + const Client = moduleReference(ClientImpl);
666 + expect(() => {
667 + const transport = ReactNoopFlightServer.render(
668 + <Client
669 + value={['looooong string takes up noise', Math, <h1>hi</h1>]}
670 + />,
671 + );
672 + ReactNoopFlightClient.read(transport);
673 + }).toErrorDev(
674 + 'Only plain objects can be passed to Client Components from Server Components. ' +
675 + 'Math objects are not supported.\n' +
676 + ' [..., Math, <h1/>]\n' +
677 + ' ^^^^',
678 + {withoutStack: true},
679 + );
680 + });
681 +
682 + it('should NOT warn in DEV for key getters', () => {
683 + const transport = ReactNoopFlightServer.render(<div key="a" />);
684 + ReactNoopFlightClient.read(transport);
685 + });
686 +
687 it('should warn in DEV if a class instance is passed to a host component', () => {
688 class Foo {
689 method() {}
@@ -519,7 +694,7 @@ describe('ReactFlight', () => {
694 );
695 ReactNoopFlightClient.read(transport);
696 }).toErrorDev(
522 - 'Only plain objects can be passed to client components from server components. ',
697 + 'Only plain objects can be passed to Client Components from Server Components. ',
698 {withoutStack: true},
699 );
700 });
@@ -577,9 +752,9 @@ describe('ReactFlight', () => {
752 });
753
754 it('[TODO] it does not warn if you render a server element passed to a client module reference twice on the client when using useId', async () => {
580 - // @TODO Today if you render a server component with useId and pass it to a client component and that client component renders the element in two or more
755 + // @TODO Today if you render a Server Component with useId and pass it to a Client Component and that Client Component renders the element in two or more
756 // places the id used on the server will be duplicated in the client. This is a deviation from the guarantees useId makes for Fizz/Client and is a consequence
582 - // of the fact that the server component is actually rendered on the server and is reduced to a set of host elements before being passed to the Client component
757 + // of the fact that the Server Component is actually rendered on the server and is reduced to a set of host elements before being passed to the Client component
758 // so the output passed to the Client has no knowledge of the useId use. In the future we would like to add a DEV warning when this happens. For now
759 // we just accept that it is a nuance of useId in Flight
760 function App() {
@@ -937,7 +1112,7 @@ describe('ReactFlight', () => {
1112
1113 expect(ClientContext).toBe(undefined);
1114
940 - // Reset all modules, except flight-modules which keeps the registry of client components
1115 + // Reset all modules, except flight-modules which keeps the registry of Client Components
1116 const flightModules = require('react-noop-renderer/flight-modules');
1117 jest.resetModules();
1118 jest.mock('react-noop-renderer/flight-modules', () => flightModules);
packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js
+3 -3
@@ -50,7 +50,7 @@ describe('ReactFlightDOMRelay', () => {
50 return model;
51 }
52
53 - it('can render a server component', () => {
53 + it('can render a Server Component', () => {
54 function Bar({text}) {
55 return text.toUpperCase();
56 }
@@ -85,7 +85,7 @@ describe('ReactFlightDOMRelay', () => {
85 });
86 });
87
88 - it('can render a client component using a module reference and render there', () => {
88 + it('can render a Client Component using a module reference and render there', () => {
89 function UserClient(props) {
90 return (
91 <span>
@@ -233,7 +233,7 @@ describe('ReactFlightDOMRelay', () => {
233 ReactDOMFlightRelayServer.render(<input value={new Foo()} />, transport);
234 readThrough(transport);
235 }).toErrorDev(
236 - 'Only plain objects can be passed to client components from server components. ',
236 + 'Only plain objects can be passed to Client Components from Server Components. ',
237 {withoutStack: true},
238 );
239 });
packages/react-server-native-relay/src/__tests__/ReactFlightNativeRelay-test.internal.js
+3 -3
@@ -61,7 +61,7 @@ describe('ReactFlightNativeRelay', () => {
61 return model;
62 }
63
64 - it('can render a server component', () => {
64 + it('can render a Server Component', () => {
65 function Bar({text}) {
66 return <Text>{text.toUpperCase()}</Text>;
67 }
@@ -86,7 +86,7 @@ describe('ReactFlightNativeRelay', () => {
86 expect(model).toMatchSnapshot();
87 });
88
89 - it('can render a client component using a module reference and render there', () => {
89 + it('can render a Client Component using a module reference and render there', () => {
90 function UserClient(props) {
91 return (
92 <Text>
@@ -132,7 +132,7 @@ describe('ReactFlightNativeRelay', () => {
132 );
133 readThrough(transport);
134 }).toErrorDev(
135 - 'Only plain objects can be passed to client components from server components. ',
135 + 'Only plain objects can be passed to Client Components from Server Components. ',
136 {withoutStack: true},
137 );
138 });
packages/react-server-native-relay/src/__tests__/__snapshots__/ReactFlightNativeRelay-test.internal.js.snap
+2 -2
@@ -1,6 +1,6 @@
1 // Jest Snapshot v1, https://goo.gl/fbAQLP
2
3 -exports[`ReactFlightNativeRelay can render a client component using a module reference and render there 1`] = `
3 +exports[`ReactFlightNativeRelay can render a Client Component using a module reference and render there 1`] = `
4 "1
5 RCTText null
6 RCTRawText {\\"text\\":\\"Hello\\"}
@@ -8,7 +8,7 @@ exports[`ReactFlightNativeRelay can render a client component using a module ref
8 RCTRawText {\\"text\\":\\"Seb Smith\\"}"
9 `;
10
11 -exports[`ReactFlightNativeRelay can render a server component 1`] = `
11 +exports[`ReactFlightNativeRelay can render a Server Component 1`] = `
12 Object {
13 "foo": Object {
14 "bar": <RCTView>
packages/react-server/src/ReactFlightServer.js
+227 -107
@@ -73,6 +73,8 @@ import {
73 REACT_LAZY_TYPE,
74 REACT_MEMO_TYPE,
75 REACT_PROVIDER_TYPE,
76 + REACT_SUSPENSE_TYPE,
77 + REACT_SUSPENSE_LIST_TYPE,
78 } from 'shared/ReactSymbols';
79
80 import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry';
@@ -195,6 +197,11 @@ function createRootContext(
197
198 const POP = {};
199
200 +// Used for DEV messages to keep track of which parent rendered some props,
201 +// in case they error.
202 +const jsxPropsParents: WeakMap<any, any> = new WeakMap();
203 +const jsxChildrenParents: WeakMap<any, any> = new WeakMap();
204 +
205 function readThenable<T>(thenable: Thenable<T>): T {
206 if (thenable.status === 'fulfilled') {
207 return thenable.value;
@@ -226,12 +233,18 @@ function attemptResolveElement(
233 // throw for functions. We could probably relax it to a DEV warning for other
234 // cases.
235 throw new Error(
229 - 'Refs cannot be used in server components, nor passed to client components.',
236 + 'Refs cannot be used in Server Components, nor passed to Client Components.',
237 );
238 }
239 + if (__DEV__) {
240 + jsxPropsParents.set(props, type);
241 + if (typeof props.children === 'object') {
242 + jsxChildrenParents.set(props.children, type);
243 + }
244 + }
245 if (typeof type === 'function') {
246 if (isModuleReference(type)) {
234 - // This is a reference to a client component.
247 + // This is a reference to a Client Component.
248 return [REACT_ELEMENT_TYPE, type, key, props];
249 }
250 // This is a server-side component.
@@ -253,7 +266,7 @@ function attemptResolveElement(
266 // For key-less fragments, we add a small optimization to avoid serializing
267 // it as a wrapper.
268 // TODO: If a key is specified, we should propagate its key to any children.
256 - // Same as if a server component has a key.
269 + // Same as if a Server Component has a key.
270 return props.children;
271 }
272 // This might be a built-in React component. We'll let the client decide.
@@ -261,7 +274,7 @@ function attemptResolveElement(
274 return [REACT_ELEMENT_TYPE, type, key, props];
275 } else if (type != null && typeof type === 'object') {
276 if (isModuleReference(type)) {
264 - // This is a reference to a client component.
277 + // This is a reference to a Client Component.
278 return [REACT_ELEMENT_TYPE, type, key, props];
279 }
280 switch (type.$$typeof) {
@@ -318,7 +331,7 @@ function attemptResolveElement(
331 }
332 }
333 throw new Error(
321 - `Unsupported server component type: ${describeValueForErrorMessage(type)}`,
334 + `Unsupported Server Component type: ${describeValueForErrorMessage(type)}`,
335 );
336 }
337
@@ -505,66 +518,184 @@ function describeValueForErrorMessage(value: ReactModel): string {
518 }
519 }
520
521 +function describeElementType(type: any): string {
522 + if (typeof type === 'string') {
523 + return type;
524 + }
525 + switch (type) {
526 + case REACT_SUSPENSE_TYPE:
527 + return 'Suspense';
528 + case REACT_SUSPENSE_LIST_TYPE:
529 + return 'SuspenseList';
530 + }
531 + if (typeof type === 'object') {
532 + switch (type.$$typeof) {
533 + case REACT_FORWARD_REF_TYPE:
534 + return describeElementType(type.render);
535 + case REACT_MEMO_TYPE:
536 + return describeElementType(type.type);
537 + case REACT_LAZY_TYPE: {
538 + const lazyComponent: LazyComponent<any, any> = (type: any);
539 + const payload = lazyComponent._payload;
540 + const init = lazyComponent._init;
541 + try {
542 + // Lazy may contain any component type so we recursively resolve it.
543 + return describeElementType(init(payload));
544 + } catch (x) {}
545 + }
546 + }
547 + }
548 + return '';
549 +}
550 +
551 function describeObjectForErrorMessage(
552 objectOrArray:
553 | {+[key: string | number]: ReactModel, ...}
554 | $ReadOnlyArray<ReactModel>,
555 expandedName?: string,
556 ): string {
557 + const objKind = objectName(objectOrArray);
558 + if (objKind !== 'Object' && objKind !== 'Array') {
559 + return objKind;
560 + }
561 + let str = '';
562 + let start = -1;
563 + let length = 0;
564 if (isArray(objectOrArray)) {
515 - let str = '[';
516 - const array: $ReadOnlyArray<ReactModel> = objectOrArray;
517 - for (let i = 0; i < array.length; i++) {
518 - if (i > 0) {
519 - str += ', ';
520 - }
521 - if (i > 6) {
522 - str += '...';
523 - break;
565 + if (__DEV__ && jsxChildrenParents.has(objectOrArray)) {
566 + // Print JSX Children
567 + const type = jsxChildrenParents.get(objectOrArray);
568 + str = '<' + describeElementType(type) + '>';
569 + const array: $ReadOnlyArray<ReactModel> = objectOrArray;
570 + for (let i = 0; i < array.length; i++) {
571 + const value = array[i];
572 + let substr;
573 + if (typeof value === 'string') {
574 + substr = value;
575 + } else if (typeof value === 'object' && value !== null) {
576 + // $FlowFixMe[incompatible-call] found when upgrading Flow
577 + substr = '{' + describeObjectForErrorMessage(value) + '}';
578 + } else {
579 + substr = '{' + describeValueForErrorMessage(value) + '}';
580 + }
581 + if ('' + i === expandedName) {
582 + start = str.length;
583 + length = substr.length;
584 + str += substr;
585 + } else if (substr.length < 15 && str.length + substr.length < 40) {
586 + str += substr;
587 + } else {
588 + str += '{...}';
589 + }
590 }
525 - const value = array[i];
526 - if (
527 - '' + i === expandedName &&
528 - typeof value === 'object' &&
529 - value !== null
530 - ) {
531 - // $FlowFixMe[incompatible-call] found when upgrading Flow
532 - str += describeObjectForErrorMessage(value);
533 - } else {
534 - str += describeValueForErrorMessage(value);
591 + str += '</' + describeElementType(type) + '>';
592 + } else {
593 + // Print Array
594 + str = '[';
595 + const array: $ReadOnlyArray<ReactModel> = objectOrArray;
596 + for (let i = 0; i < array.length; i++) {
597 + if (i > 0) {
598 + str += ', ';
599 + }
600 + const value = array[i];
601 + let substr;
602 + if (typeof value === 'object' && value !== null) {
603 + // $FlowFixMe[incompatible-call] found when upgrading Flow
604 + substr = describeObjectForErrorMessage(value);
605 + } else {
606 + substr = describeValueForErrorMessage(value);
607 + }
608 + if ('' + i === expandedName) {
609 + start = str.length;
610 + length = substr.length;
611 + str += substr;
612 + } else if (substr.length < 10 && str.length + substr.length < 40) {
613 + str += substr;
614 + } else {
615 + str += '...';
616 + }
617 }
618 + str += ']';
619 }
537 - str += ']';
538 - return str;
620 } else {
540 - let str = '{';
541 - const object: {+[key: string | number]: ReactModel, ...} = objectOrArray;
542 - const names = Object.keys(object);
543 - for (let i = 0; i < names.length; i++) {
544 - if (i > 0) {
545 - str += ', ';
546 - }
547 - if (i > 6) {
548 - str += '...';
549 - break;
621 + if (objectOrArray.$$typeof === REACT_ELEMENT_TYPE) {
622 + str = '<' + describeElementType(objectOrArray.type) + '/>';
623 + } else if (__DEV__ && jsxPropsParents.has(objectOrArray)) {
624 + // Print JSX
625 + const type = jsxPropsParents.get(objectOrArray);
626 + str = '<' + (describeElementType(type) || '...');
627 + const object: {+[key: string | number]: ReactModel, ...} = objectOrArray;
628 + const names = Object.keys(object);
629 + for (let i = 0; i < names.length; i++) {
630 + str += ' ';
631 + const name = names[i];
632 + str += describeKeyForErrorMessage(name) + '=';
633 + const value = object[name];
634 + let substr;
635 + if (
636 + name === expandedName &&
637 + typeof value === 'object' &&
638 + value !== null
639 + ) {
640 + // $FlowFixMe[incompatible-call] found when upgrading Flow
641 + substr = describeObjectForErrorMessage(value);
642 + } else {
643 + substr = describeValueForErrorMessage(value);
644 + }
645 + if (typeof value !== 'string') {
646 + substr = '{' + substr + '}';
647 + }
648 + if (name === expandedName) {
649 + start = str.length;
650 + length = substr.length;
651 + str += substr;
652 + } else if (substr.length < 10 && str.length + substr.length < 40) {
653 + str += substr;
654 + } else {
655 + str += '...';
656 + }
657 }
551 - const name = names[i];
552 - str += describeKeyForErrorMessage(name) + ': ';
553 - const value = object[name];
554 - if (
555 - name === expandedName &&
556 - typeof value === 'object' &&
557 - value !== null
558 - ) {
559 - // $FlowFixMe[incompatible-call] found when upgrading Flow
560 - str += describeObjectForErrorMessage(value);
561 - } else {
562 - str += describeValueForErrorMessage(value);
658 + str += '>';
659 + } else {
660 + // Print Object
661 + str = '{';
662 + const object: {+[key: string | number]: ReactModel, ...} = objectOrArray;
663 + const names = Object.keys(object);
664 + for (let i = 0; i < names.length; i++) {
665 + if (i > 0) {
666 + str += ', ';
667 + }
668 + const name = names[i];
669 + str += describeKeyForErrorMessage(name) + ': ';
670 + const value = object[name];
671 + let substr;
672 + if (typeof value === 'object' && value !== null) {
673 + // $FlowFixMe[incompatible-call] found when upgrading Flow
674 + substr = describeObjectForErrorMessage(value);
675 + } else {
676 + substr = describeValueForErrorMessage(value);
677 + }
678 + if (name === expandedName) {
679 + start = str.length;
680 + length = substr.length;
681 + str += substr;
682 + } else if (substr.length < 10 && str.length + substr.length < 40) {
683 + str += substr;
684 + } else {
685 + str += '...';
686 + }
687 }
688 + str += '}';
689 }
565 - str += '}';
690 + }
691 + if (expandedName === undefined) {
692 return str;
693 }
694 + if (start > -1 && length > 0) {
695 + const highlight = ' '.repeat(start) + '^'.repeat(length);
696 + return '\n ' + str + '\n ' + highlight;
697 + }
698 + return '\n ' + str;
699 }
700
701 let insideContextProps = null;
@@ -580,14 +711,30 @@ export function resolveModelToJSON(
711 // $FlowFixMe
712 const originalValue = parent[key];
713 if (typeof originalValue === 'object' && originalValue !== value) {
583 - console.error(
584 - 'Only plain objects can be passed to client components from server components. ' +
585 - 'Objects with toJSON methods are not supported. Convert it manually ' +
586 - 'to a simple value before passing it to props. ' +
587 - 'Remove %s from these props: %s',
588 - describeKeyForErrorMessage(key),
589 - describeObjectForErrorMessage(parent),
590 - );
714 + if (objectName(originalValue) !== 'Object') {
715 + const jsxParentType = jsxChildrenParents.get(parent);
716 + if (typeof jsxParentType === 'string') {
717 + console.error(
718 + '%s objects cannot be rendered as text children. Try formatting it using toString().%s',
719 + objectName(originalValue),
720 + describeObjectForErrorMessage(parent, key),
721 + );
722 + } else {
723 + console.error(
724 + 'Only plain objects can be passed to Client Components from Server Components. ' +
725 + '%s objects are not supported.%s',
726 + objectName(originalValue),
727 + describeObjectForErrorMessage(parent, key),
728 + );
729 + }
730 + } else {
731 + console.error(
732 + 'Only plain objects can be passed to Client Components from Server Components. ' +
733 + 'Objects with toJSON methods are not supported. Convert it manually ' +
734 + 'to a simple value before passing it to props.%s',
735 + describeObjectForErrorMessage(parent, key),
736 + );
737 + }
738 }
739 }
740
@@ -612,7 +759,7 @@ export function resolveModelToJSON(
759 }
760 }
761
615 - // Resolve server components.
762 + // Resolve Server Components.
763 while (
764 typeof value === 'object' &&
765 value !== null &&
@@ -630,7 +777,7 @@ export function resolveModelToJSON(
777 case REACT_ELEMENT_TYPE: {
778 // TODO: Concatenate keys of parents onto children.
779 const element: React$Element<any> = (value: any);
633 - // Attempt to render the server component.
780 + // Attempt to render the Server Component.
781 value = attemptResolveElement(
782 element.type,
783 element.key,
@@ -716,30 +863,24 @@ export function resolveModelToJSON(
863 // Verify that this is a simple plain object.
864 if (objectName(value) !== 'Object') {
865 console.error(
719 - 'Only plain objects can be passed to client components from server components. ' +
720 - 'Built-ins like %s are not supported. ' +
721 - 'Remove %s from these props: %s',
866 + 'Only plain objects can be passed to Client Components from Server Components. ' +
867 + '%s objects are not supported.%s',
868 objectName(value),
723 - describeKeyForErrorMessage(key),
724 - describeObjectForErrorMessage(parent),
869 + describeObjectForErrorMessage(parent, key),
870 );
871 } else if (!isSimpleObject(value)) {
872 console.error(
728 - 'Only plain objects can be passed to client components from server components. ' +
729 - 'Classes or other objects with methods are not supported. ' +
730 - 'Remove %s from these props: %s',
731 - describeKeyForErrorMessage(key),
873 + 'Only plain objects can be passed to Client Components from Server Components. ' +
874 + 'Classes or other objects with methods are not supported.%s',
875 describeObjectForErrorMessage(parent, key),
876 );
877 } else if (Object.getOwnPropertySymbols) {
878 const symbols = Object.getOwnPropertySymbols(value);
879 if (symbols.length > 0) {
880 console.error(
738 - 'Only plain objects can be passed to client components from server components. ' +
739 - 'Objects with symbol properties like %s are not supported. ' +
740 - 'Remove %s from these props: %s',
881 + 'Only plain objects can be passed to Client Components from Server Components. ' +
882 + 'Objects with symbol properties like %s are not supported.%s',
883 symbols[0].description,
742 - describeKeyForErrorMessage(key),
884 describeObjectForErrorMessage(parent, key),
885 );
886 }
@@ -769,24 +910,15 @@ export function resolveModelToJSON(
910 }
911 if (/^on[A-Z]/.test(key)) {
912 throw new Error(
772 - 'Event handlers cannot be passed to client component props. ' +
773 - `Remove ${describeKeyForErrorMessage(
774 - key,
775 - )} from these props if possible: ${describeObjectForErrorMessage(
776 - parent,
777 - )}
778 -` +
779 - 'If you need interactivity, consider converting part of this to a client component.',
913 + 'Event handlers cannot be passed to Client Component props.' +
914 + describeObjectForErrorMessage(parent, key) +
915 + '\nIf you need interactivity, consider converting part of this to a Client Component.',
916 );
917 } else {
918 throw new Error(
783 - 'Functions cannot be passed directly to client components ' +
784 - "because they're not serializable. " +
785 - `Remove ${describeKeyForErrorMessage(key)} (${value.displayName ||
786 - value.name ||
787 - 'function'}) from this object, or avoid the entire object: ${describeObjectForErrorMessage(
788 - parent,
789 - )}`,
919 + 'Functions cannot be passed directly to Client Components ' +
920 + "because they're not serializable." +
921 + describeObjectForErrorMessage(parent, key),
922 );
923 }
924 }
@@ -802,16 +934,12 @@ export function resolveModelToJSON(
934
935 if (Symbol.for(name) !== value) {
936 throw new Error(
805 - 'Only global symbols received from Symbol.for(...) can be passed to client components. ' +
937 + 'Only global symbols received from Symbol.for(...) can be passed to Client Components. ' +
938 `The symbol Symbol.for(${
939 // $FlowFixMe `description` might be undefined
940 value.description
809 - }) cannot be found among global symbols. ` +
810 - `Remove ${describeKeyForErrorMessage(
811 - key,
812 - )} from this object, or avoid the entire object: ${describeObjectForErrorMessage(
813 - parent,
814 - )}`,
941 + }) cannot be found among global symbols.` +
942 + describeObjectForErrorMessage(parent, key),
943 );
944 }
945
@@ -825,22 +953,14 @@ export function resolveModelToJSON(
953 // $FlowFixMe: bigint isn't added to Flow yet.
954 if (typeof value === 'bigint') {
955 throw new Error(
828 - `BigInt (${value}) is not yet supported in client component props. ` +
829 - `Remove ${describeKeyForErrorMessage(
830 - key,
831 - )} from this object or use a plain number instead: ${describeObjectForErrorMessage(
832 - parent,
833 - )}`,
956 + `BigInt (${value}) is not yet supported in Client Component props.` +
957 + describeObjectForErrorMessage(parent, key),
958 );
959 }
960
961 throw new Error(
838 - `Type ${typeof value} is not supported in client component props. ` +
839 - `Remove ${describeKeyForErrorMessage(
840 - key,
841 - )} from this object, or avoid the entire object: ${describeObjectForErrorMessage(
842 - parent,
843 - )}`,
962 + `Type ${typeof value} is not supported in Client Component props.` +
963 + describeObjectForErrorMessage(parent, key),
964 );
965 }
966
@@ -968,7 +1088,7 @@ function retryTask(request: Request, task: Task): void {
1088 // previous attempt.
1089 const prevThenableState = task.thenableState;
1090
971 - // Attempt to render the server component.
1091 + // Attempt to render the Server Component.
1092 // Doing this here lets us reuse this same task if the next component
1093 // also suspends.
1094 task.model = value;
scripts/error-codes/codes.json
+7 -7
@@ -338,7 +338,7 @@
338 "348": "ensureListeningTo(): received a container that was not an element node. This is likely a bug in React.",
339 "349": "Expected a work-in-progress root. This is a bug in React. Please file an issue.",
340 "350": "Cannot read from mutable source during the current render without tearing. This may be a bug in React. Please file an issue.",
341 - "351": "Unsupported server component type: %s",
341 + "351": "Unsupported Server Component type: %s",
342 "352": "React Lazy Components are not yet supported on the server.",
343 "353": "A server block should never encode any other slots. This is a bug in React.",
344 "354": "getInspectorDataForViewAtPoint() is not available in production.",
@@ -360,12 +360,12 @@
360 "371": "Text string must be rendered within a <Text> component.\n\nText: %s",
361 "372": "Cannot call unstable_createEventHandle with \"%s\", as it is not an event known to React.",
362 "373": "This Hook is not supported in Server Components.",
363 - "374": "Event handlers cannot be passed to client component props. Remove %s from these props if possible: %s\nIf you need interactivity, consider converting part of this to a client component.",
364 - "375": "Functions cannot be passed directly to client components because they're not serializable. Remove %s (%s) from this object, or avoid the entire object: %s",
365 - "376": "Only global symbols received from Symbol.for(...) can be passed to client components. The symbol Symbol.for(%s) cannot be found among global symbols. Remove %s from this object, or avoid the entire object: %s",
366 - "377": "BigInt (%s) is not yet supported in client component props. Remove %s from this object or use a plain number instead: %s",
367 - "378": "Type %s is not supported in client component props. Remove %s from this object, or avoid the entire object: %s",
368 - "379": "Refs cannot be used in server components, nor passed to client components.",
363 + "374": "Event handlers cannot be passed to Client Component props.%s\nIf you need interactivity, consider converting part of this to a Client Component.",
364 + "375": "Functions cannot be passed directly to Client Components because they're not serializable.%s",
365 + "376": "Only global symbols received from Symbol.for(...) can be passed to Client Components. The symbol Symbol.for(%s) cannot be found among global symbols.%s",
366 + "377": "BigInt (%s) is not yet supported in Client Component props.%s",
367 + "378": "Type %s is not supported in Client Component props.%s",
368 + "379": "Refs cannot be used in Server Components, nor passed to Client Components.",
369 "380": "Reading the cache is only supported while rendering.",
370 "381": "This feature is not supported by ReactSuspenseTestUtils.",
371 "382": "This query has received more parameters than the last time the same query was used. Always pass the exact number of parameters that the query needs.",