diff --git a/.changeset/error-function-jsx-children.md b/.changeset/error-function-jsx-children.md new file mode 100644 index 00000000000..a147f48f26c --- /dev/null +++ b/.changeset/error-function-jsx-children.md @@ -0,0 +1,5 @@ +--- +'@builder.io/qwik': patch +--- + +Throw an error when a function is passed as a JSX child instead of silently skipping it. This restores the v1 behavior where passing `{myFn}` instead of `{myFn()}` would produce a clear error message. diff --git a/packages/qwik/src/core/render/dom/render-dom.ts b/packages/qwik/src/core/render/dom/render-dom.ts index 916469a54aa..44248f1d9b5 100644 --- a/packages/qwik/src/core/render/dom/render-dom.ts +++ b/packages/qwik/src/core/render/dom/render-dom.ts @@ -176,6 +176,12 @@ export const processData = ( return node.then((node) => processData(node, invocationContext)); } else if (node === SkipRender) { return new ProcessedJSXNodeImpl(SKIP_RENDER_TYPE, EMPTY_OBJ, null, EMPTY_ARRAY, 0, null); + } else if (isFunction(node)) { + throw new Error( + `Functions are not valid JSX children. ` + + `You might have passed a function instead of calling it: use {fn()} instead of {fn}. ` + + `If this is a component, wrap it with component$().` + ); } else { logWarn('A unsupported value was passed to the JSX, skipping render. Value:', node); return undefined; diff --git a/packages/qwik/src/core/render/dom/render.unit.tsx b/packages/qwik/src/core/render/dom/render.unit.tsx index 60fc11825a1..ddaf0d47f2b 100644 --- a/packages/qwik/src/core/render/dom/render.unit.tsx +++ b/packages/qwik/src/core/render/dom/render.unit.tsx @@ -44,6 +44,16 @@ test('should only render string/number', async () => { await expectRendered(fixture, '
string123
'); }); +test('should throw error when function is passed as JSX child', async () => { + const fixture = new ElementFixture(); + try { + await render(fixture.host,
{() => hello}
); + assert.fail('Expected an error to be thrown'); + } catch (e: any) { + assert.match(e.message, /is not an accepted value|Functions are not valid JSX children/); + } +}); + test('should serialize events correctly', async () => { const fixture = new ElementFixture(); await render( diff --git a/packages/qwik/src/core/render/ssr/render-ssr.ts b/packages/qwik/src/core/render/ssr/render-ssr.ts index 07bcf397326..54edfd72077 100644 --- a/packages/qwik/src/core/render/ssr/render-ssr.ts +++ b/packages/qwik/src/core/render/ssr/render-ssr.ts @@ -923,6 +923,12 @@ const processData = ( } else if (isPromise(node)) { stream.write(FLUSH_COMMENT); return node.then((node) => processData(node, rCtx, ssrCtx, stream, flags, beforeClose)); + } else if (isFunction(node)) { + throw new Error( + `Functions are not valid JSX children. ` + + `You might have passed a function instead of calling it: use {fn()} instead of {fn}. ` + + `If this is a component, wrap it with component$().` + ); } else { logWarn('A unsupported value was passed to the JSX, skipping render. Value:', node); return; diff --git a/packages/qwik/src/core/render/ssr/render-ssr.unit.tsx b/packages/qwik/src/core/render/ssr/render-ssr.unit.tsx index ebd7b01035f..d5cb750f113 100644 --- a/packages/qwik/src/core/render/ssr/render-ssr.unit.tsx +++ b/packages/qwik/src/core/render/ssr/render-ssr.unit.tsx @@ -1968,6 +1968,11 @@ export const HtmlContext = component$(() => { return ; }); +test('should throw error when function is passed as JSX child', async () => { + const fn = () => jsx('div', { children: 'hello' }); + await throws(() => testSSR(jsx('body', { children: [fn] }), '')); +}); + async function testSSR( node: JSXOutput, expected: string | string[],