Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Flight] Better error message if you pass a function as a child to a client component #28367

Merged
merged 1 commit into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,7 @@ describe('ReactFlight', () => {
);
}
function FunctionProp() {
return <div>{() => {}}</div>;
return <div>{function fn() {}}</div>;
}
function SymbolProp() {
return <div foo={Symbol('foo')} />;
Expand All @@ -707,8 +707,11 @@ describe('ReactFlight', () => {
</Client>
);
}
function FunctionChildrenClient() {
return <Client>{function Component() {}}</Client>;
}
function FunctionPropClient() {
return <Client>{() => {}}</Client>;
return <Client foo={() => {}} />;
}
function SymbolPropClient() {
return <Client foo={Symbol('foo')} />;
Expand All @@ -731,6 +734,10 @@ describe('ReactFlight', () => {
<EventHandlerPropClient />,
options,
);
const fnChildrenClient = ReactNoopFlightServer.render(
<FunctionChildrenClient />,
options,
);
const fnClient = ReactNoopFlightServer.render(
<FunctionPropClient />,
options,
Expand All @@ -754,7 +761,9 @@ describe('ReactFlight', () => {
</ErrorBoundary>
<ErrorBoundary
expectedMessage={
'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
__DEV__
? 'Functions are not valid as a child of Client Components. This may happen if you return fn instead of <fn /> from render. Or maybe you meant to call this function rather than return it.'
: 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
}>
<Render promise={ReactNoopFlightClient.read(fn)} />
</ErrorBoundary>
Expand All @@ -767,6 +776,14 @@ describe('ReactFlight', () => {
<ErrorBoundary expectedMessage="Event handlers cannot be passed to Client Component props.">
<Render promise={ReactNoopFlightClient.read(eventClient)} />
</ErrorBoundary>
<ErrorBoundary
expectedMessage={
__DEV__
? 'Functions are not valid as a child of Client Components. This may happen if you return Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.'
: 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
}>
<Render promise={ReactNoopFlightClient.read(fnChildrenClient)} />
</ErrorBoundary>
<ErrorBoundary
expectedMessage={
'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
Expand Down Expand Up @@ -945,8 +962,8 @@ describe('ReactFlight', () => {
'Only plain objects can be passed to Client Components from Server Components. ' +
'Objects with toJSON methods are not supported. ' +
'Convert it manually to a simple value before passing it to props.\n' +
' <input value={{toJSON: function}}>\n' +
' ^^^^^^^^^^^^^^^^^^^^',
' <input value={{toJSON: ...}}>\n' +
' ^^^^^^^^^^^^^^^',
{withoutStack: true},
);
});
Expand Down Expand Up @@ -1035,8 +1052,8 @@ describe('ReactFlight', () => {
'Only plain objects can be passed to Client Components from Server Components. ' +
'Objects with toJSON methods are not supported. ' +
'Convert it manually to a simple value before passing it to props.\n' +
' <>Current date: {{toJSON: function}}</>\n' +
' ^^^^^^^^^^^^^^^^^^^^',
' <>Current date: {{toJSON: ...}}</>\n' +
' ^^^^^^^^^^^^^^^',
{withoutStack: true},
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1710,15 +1710,17 @@ describe('ReactFlightDOM', () => {
expect(reportedErrors.length).toBe(1);
if (__DEV__) {
expect(reportedErrors[0].message).toEqual(
'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".\n' +
' <... prop={client} invalid={function}>\n' +
' ^^^^^^^^^^',
'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". ' +
'Or maybe you meant to call this function rather than return it.\n' +
' <... prop={client} invalid={function InvalidValue}>\n' +
' ^^^^^^^^^^^^^^^^^^^^^^^',
);
} else {
expect(reportedErrors[0].message).toEqual(
'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".\n' +
' {prop: client, invalid: function}\n' +
' ^^^^^^^^',
'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". ' +
'Or maybe you meant to call this function rather than return it.\n' +
' {prop: client, invalid: function InvalidValue}\n' +
' ^^^^^^^^^^^^^^^^^^^^^',
);
}
});
Expand Down
19 changes: 18 additions & 1 deletion packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1554,10 +1554,27 @@ function renderModelDestructive(
describeObjectForErrorMessage(parent, parentPropertyName) +
'\nIf you need interactivity, consider converting part of this to a Client Component.',
);
} else if (
__DEV__ &&
(jsxChildrenParents.has(parent) ||
(jsxPropsParents.has(parent) && parentPropertyName === 'children'))
) {
const componentName = value.displayName || value.name || 'Component';
throw new Error(
'Functions are not valid as a child of Client Components. This may happen if ' +
'you return ' +
componentName +
' instead of <' +
componentName +
' /> from render. ' +
'Or maybe you meant to call this function rather than return it.' +
describeObjectForErrorMessage(parent, parentPropertyName),
);
} else {
throw new Error(
'Functions cannot be passed directly to Client Components ' +
'unless you explicitly expose it by marking it with "use server".' +
'unless you explicitly expose it by marking it with "use server". ' +
'Or maybe you meant to call this function rather than return it.' +
describeObjectForErrorMessage(parent, parentPropertyName),
);
}
Expand Down
6 changes: 4 additions & 2 deletions packages/shared/ReactSerializationErrors.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,13 @@ export function describeValueForErrorMessage(value: mixed): string {
}
return name;
}
case 'function':
case 'function': {
if ((value: any).$$typeof === CLIENT_REFERENCE_TAG) {
return describeClientReference(value);
}
return 'function';
const name = (value: any).displayName || value.name;
return name ? 'function ' + name : 'function';
}
default:
// eslint-disable-next-line react-internal/safe-string-coercion
return String(value);
Expand Down
5 changes: 3 additions & 2 deletions scripts/error-codes/codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@
"372": "Cannot call unstable_createEventHandle with \"%s\", as it is not an event known to React.",
"373": "This Hook is not supported in Server Components.",
"374": "Event handlers cannot be passed to Client Component props.%s\nIf you need interactivity, consider converting part of this to a Client Component.",
"375": "Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with \"use server\".%s",
"375": "Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with \"use server\". Or maybe you meant to call this function rather than return it.%s",
"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",
"377": "BigInt (%s) is not yet supported in Client Component props.%s",
"378": "Type %s is not supported in Client Component props.%s",
Expand Down Expand Up @@ -490,5 +490,6 @@
"502": "Cannot read a Client Context from a Server Component.",
"503": "Cannot use() an already resolved Client Reference.",
"504": "Failed to read a RSC payload created by a development version of React on the server while using a production version on the client. Always use matching versions on the server and the client.",
"505": "Cannot render an Async Component, Promise or React.Lazy inside React.Children. We recommend not iterating over children and just rendering them plain."
"505": "Cannot render an Async Component, Promise or React.Lazy inside React.Children. We recommend not iterating over children and just rendering them plain.",
"506": "Functions are not valid as a child of Client Components. This may happen if you return %s instead of <%s /> from render. Or maybe you meant to call this function rather than return it.%s"
}