Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/fix-ssr-show-switch-zero-arg-children.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"solid-js": patch
---

Fix server `Show`/`Switch` to ignore zero-arg function children, matching client behavior per #1508.
8 changes: 5 additions & 3 deletions packages/solid/src/server/rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,9 @@ export function Show<T>(props: {
}): string {
let c: string | ((item: NonNullable<T> | Accessor<NonNullable<T>>) => 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 || "";
}

Expand All @@ -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 || "";
Expand Down
53 changes: 52 additions & 1 deletion packages/solid/test/rendering.spec.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<unknown>({ 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<unknown>({
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<unknown>({ when: "hello", children: (item: any) => `got ${item()}` })
});
expect(result).toBe("got hello");
});
});