Skip to content

Commit bc35836

Browse files
authored
[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
1 parent 3ba788f commit bc35836

File tree

6 files changed

+445
-150
lines changed

6 files changed

+445
-150
lines changed

packages/react-client/src/__tests__/ReactFlight-test.js

+203-28
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ describe('ReactFlight', () => {
9595
};
9696
}
9797

98-
it('can render a server component', async () => {
98+
it('can render a Server Component', async () => {
9999
function Bar({text}) {
100100
return text.toUpperCase();
101101
}
@@ -125,7 +125,7 @@ describe('ReactFlight', () => {
125125
});
126126
});
127127

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 () => {
129129
function UserClient(props) {
130130
return (
131131
<span>
@@ -363,6 +363,11 @@ describe('ReactFlight', () => {
363363

364364
// @gate enableUseHook
365365
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+
366371
function EventHandlerProp() {
367372
return (
368373
<div className="foo" onClick={function() {}}>
@@ -382,6 +387,24 @@ describe('ReactFlight', () => {
382387
return <div ref={ref} />;
383388
}
384389

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+
385408
const options = {
386409
onError(x) {
387410
return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
@@ -391,26 +414,51 @@ describe('ReactFlight', () => {
391414
const fn = ReactNoopFlightServer.render(<FunctionProp />, options);
392415
const symbol = ReactNoopFlightServer.render(<SymbolProp />, options);
393416
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);
394430

395-
function Client({promise}) {
431+
function Render({promise}) {
396432
return use(promise);
397433
}
398434

399435
await act(async () => {
400436
startTransition(() => {
401437
ReactNoop.render(
402438
<>
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)} />
405453
</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)} />
408456
</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)} />
411459
</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)} />
414462
</ErrorBoundary>
415463
</>,
416464
);
@@ -419,19 +467,19 @@ describe('ReactFlight', () => {
419467
});
420468

421469
// @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 () => {
423471
function ServerComponent() {
424-
throw new Error('This was thrown in the server component.');
472+
throw new Error('This was thrown in the Server Component.');
425473
}
426474

427475
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.
429477
// We currently need to wrap it in a div because as it's set up right now, a lazy reference will
430478
// throw during reconciliation which will trigger the parent of the error boundary.
431479
// This is similar to how these will suspend the parent if it's a direct child of a Suspense boundary.
432480
// That's a bug.
433481
return (
434-
<ErrorBoundary expectedMessage="This was thrown in the server component.">
482+
<ErrorBoundary expectedMessage="This was thrown in the Server Component.">
435483
<div>{children}</div>
436484
</ErrorBoundary>
437485
);
@@ -475,25 +523,37 @@ describe('ReactFlight', () => {
475523
);
476524
ReactNoopFlightClient.read(transport);
477525
}).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.',
479528
{withoutStack: true},
480529
);
481530
});
482531

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', () => {
484533
expect(() => {
485-
const transport = ReactNoopFlightServer.render(<input value={Math} />);
534+
const transport = ReactNoopFlightServer.render(
535+
<div>Current date: {new Date()}</div>,
536+
);
486537
ReactNoopFlightClient.read(transport);
487538
}).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+
' ^^^^^^',
490542
{withoutStack: true},
491543
);
492544
});
493545

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+
);
497557
});
498558

499559
it('should warn in DEV if an object with symbols is passed to a host component', () => {
@@ -503,12 +563,127 @@ describe('ReactFlight', () => {
503563
);
504564
ReactNoopFlightClient.read(transport);
505565
}).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. ' +
507567
'Objects with symbol properties like Symbol.iterator are not supported.',
508568
{withoutStack: true},
509569
);
510570
});
511571

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+
512687
it('should warn in DEV if a class instance is passed to a host component', () => {
513688
class Foo {
514689
method() {}
@@ -519,7 +694,7 @@ describe('ReactFlight', () => {
519694
);
520695
ReactNoopFlightClient.read(transport);
521696
}).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. ',
523698
{withoutStack: true},
524699
);
525700
});
@@ -577,9 +752,9 @@ describe('ReactFlight', () => {
577752
});
578753

579754
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
581756
// 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
583758
// 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
584759
// we just accept that it is a nuance of useId in Flight
585760
function App() {
@@ -937,7 +1112,7 @@ describe('ReactFlight', () => {
9371112

9381113
expect(ClientContext).toBe(undefined);
9391114

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
9411116
const flightModules = require('react-noop-renderer/flight-modules');
9421117
jest.resetModules();
9431118
jest.mock('react-noop-renderer/flight-modules', () => flightModules);

packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('ReactFlightDOMRelay', () => {
5050
return model;
5151
}
5252

53-
it('can render a server component', () => {
53+
it('can render a Server Component', () => {
5454
function Bar({text}) {
5555
return text.toUpperCase();
5656
}
@@ -85,7 +85,7 @@ describe('ReactFlightDOMRelay', () => {
8585
});
8686
});
8787

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', () => {
8989
function UserClient(props) {
9090
return (
9191
<span>
@@ -233,7 +233,7 @@ describe('ReactFlightDOMRelay', () => {
233233
ReactDOMFlightRelayServer.render(<input value={new Foo()} />, transport);
234234
readThrough(transport);
235235
}).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. ',
237237
{withoutStack: true},
238238
);
239239
});

packages/react-server-native-relay/src/__tests__/ReactFlightNativeRelay-test.internal.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe('ReactFlightNativeRelay', () => {
6161
return model;
6262
}
6363

64-
it('can render a server component', () => {
64+
it('can render a Server Component', () => {
6565
function Bar({text}) {
6666
return <Text>{text.toUpperCase()}</Text>;
6767
}
@@ -86,7 +86,7 @@ describe('ReactFlightNativeRelay', () => {
8686
expect(model).toMatchSnapshot();
8787
});
8888

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', () => {
9090
function UserClient(props) {
9191
return (
9292
<Text>
@@ -132,7 +132,7 @@ describe('ReactFlightNativeRelay', () => {
132132
);
133133
readThrough(transport);
134134
}).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. ',
136136
{withoutStack: true},
137137
);
138138
});

0 commit comments

Comments
 (0)