diff --git a/.changeset/fix-ssr-show-switch-zero-arg-children.md b/.changeset/fix-ssr-show-switch-zero-arg-children.md new file mode 100644 index 000000000..839217de1 --- /dev/null +++ b/.changeset/fix-ssr-show-switch-zero-arg-children.md @@ -0,0 +1,5 @@ +--- +"solid-js": patch +--- + +Fix server `Show`/`Switch` to ignore zero-arg function children, matching client behavior per #1508. diff --git a/packages/solid/src/server/rendering.ts b/packages/solid/src/server/rendering.ts index 8cdfd4c55..810d94746 100644 --- a/packages/solid/src/server/rendering.ts +++ b/packages/solid/src/server/rendering.ts @@ -302,9 +302,9 @@ export function Show(props: { }): string { let c: string | ((item: NonNullable | Accessor>) => string); return props.when - ? typeof (c = props.children) === "function" + ? typeof (c = props.children) === "function" && c.length > 0 ? c(props.keyed ? props.when! : () => props.when as any) - : c + : (c as string) : props.fallback || ""; } @@ -319,7 +319,9 @@ export function Switch(props: { const w = conditions[i].when; if (w) { const c = conditions[i].children; - return typeof c === "function" ? c(conditions[i].keyed ? w : () => w) : c; + return typeof c === "function" && c.length > 0 + ? c(conditions[i].keyed ? w : () => w) + : (c as string); } } return props.fallback || ""; diff --git a/packages/solid/test/rendering.spec.ts b/packages/solid/test/rendering.spec.ts index 0a0ab3ce5..0be226850 100644 --- a/packages/solid/test/rendering.spec.ts +++ b/packages/solid/test/rendering.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "vitest"; import { createResource } from "../src/index.js"; -import { mergeProps } from "../src/server/rendering.js"; +import { mergeProps, Show, Switch, Match } from "../src/server/rendering.js"; import { resolveSSRNode } from "dom-expressions/src/server.js"; describe("resolveSSRNode", () => { @@ -51,6 +51,7 @@ describe("createResource", () => { expect(data.loading).toBe(false); }); }); + describe("server mergeProps", () => { test("preserves properties that shadow Object.prototype methods (toString, valueOf)", () => { expect(mergeProps({ toString: 1 }).toString).toBe(1); @@ -78,3 +79,53 @@ describe("server mergeProps", () => { expect(props.b).toBe(3); }); }); + +// A zero-arg function child is treated as a static value rather than a render-prop +// (see solidjs/solid#1508), since there's nothing for it to receive as an argument. +// The client (packages/solid/src/render/flow.ts) gates on `child.length > 0` before +// invoking it as a render-prop; the server implementation needs to agree, or markup +// rendered with renderToString can diverge from what hydration on the client produces. +describe("Show with a zero-arg function child", () => { + test("is treated like a plain value, same as ErrorBoundary's fallback", () => { + function staticChild() { + return "static content"; + } + expect(Show({ when: true, children: staticChild as any })).toBe(staticChild as any); + }); + + test("a render-prop (arity > 0) is still invoked with the value", () => { + expect(Show({ when: "hello", keyed: true, children: (item: any) => `got ${item}` })).toBe( + "got hello" + ); + // non-keyed: the render-prop receives an accessor, not the raw value + expect(Show({ when: "hello", children: (item: any) => `got ${item()}` })).toBe("got hello"); + }); +}); + +describe("Switch/Match with a zero-arg function child", () => { + test("is treated like a plain value, same as ErrorBoundary's fallback", () => { + function staticChild() { + return "static content"; + } + const result = Switch({ + children: Match({ when: true, children: staticChild as any }) + }); + expect(result).toBe(staticChild as any); + }); + + test("a render-prop (arity > 0) is still invoked with the value", () => { + const keyedResult = Switch({ + children: Match({ + when: "hello", + keyed: true, + children: (item: any) => `got ${item}` + }) + }); + expect(keyedResult).toBe("got hello"); + // non-keyed: the render-prop receives an accessor, not the raw value + const result = Switch({ + children: Match({ when: "hello", children: (item: any) => `got ${item()}` }) + }); + expect(result).toBe("got hello"); + }); +});