Skip to content

Commit 142adca

Browse files
Support lazy fallback in <Show>
1 parent 5a85452 commit 142adca

4 files changed

Lines changed: 124 additions & 4 deletions

File tree

packages/preact/utils/src/index.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useMemo } from "preact/hooks";
55

66
interface ShowProps<T = boolean> {
77
when: Signal<T> | ReadonlySignal<T> | (() => T);
8-
fallback?: ComponentChildren;
8+
fallback?: ComponentChildren | (() => ComponentChildren);
99
children: ComponentChildren | ((value: NonNullable<T>) => ComponentChildren);
1010
}
1111

@@ -22,7 +22,10 @@ export function Show<T = boolean>(
2222
): ComponentChildren | null {
2323
const value =
2424
typeof props.when === "function" ? props.when() : props.when.value;
25-
if (!value) return props.fallback || null;
25+
if (!value) {
26+
const fallback = props.fallback;
27+
return typeof fallback === "function" ? fallback() : fallback || null;
28+
}
2629
return <Item v={value} children={props.children} />;
2730
}
2831

packages/preact/utils/test/browser/index.test.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,63 @@ describe("@preact/signals-utils", () => {
6161
expect(scratch.innerHTML).to.eq("<p>Showing 2</p>");
6262
});
6363

64+
it("Should call function fallback lazily", () => {
65+
const toggle = signal(true)!;
66+
let fallbackCalled = false;
67+
68+
act(() => {
69+
render(
70+
<Show
71+
when={toggle}
72+
fallback={() => {
73+
fallbackCalled = true;
74+
return <p>Hidden</p>;
75+
}}
76+
>
77+
<p>Shown</p>
78+
</Show>,
79+
scratch
80+
);
81+
});
82+
83+
// When condition is true, fallback should NOT have been called
84+
expect(fallbackCalled).to.eq(false);
85+
expect(scratch.innerHTML).to.eq("<p>Shown</p>");
86+
87+
act(() => {
88+
toggle.value = false;
89+
});
90+
91+
// Now fallback should have been called
92+
expect(fallbackCalled).to.eq(true);
93+
expect(scratch.innerHTML).to.eq("<p>Hidden</p>");
94+
});
95+
96+
it("Should reactively show with lazy function fallback", () => {
97+
const toggle = signal(false)!;
98+
const Paragraph = (props: any) => <p>{props.children}</p>;
99+
100+
act(() => {
101+
render(
102+
<Show when={toggle} fallback={() => <Paragraph>Hiding</Paragraph>}>
103+
<Paragraph>Showing</Paragraph>
104+
</Show>,
105+
scratch
106+
);
107+
});
108+
expect(scratch.innerHTML).to.eq("<p>Hiding</p>");
109+
110+
act(() => {
111+
toggle.value = true;
112+
});
113+
expect(scratch.innerHTML).to.eq("<p>Showing</p>");
114+
115+
act(() => {
116+
toggle.value = false;
117+
});
118+
expect(scratch.innerHTML).to.eq("<p>Hiding</p>");
119+
});
120+
64121
it("Should preserve signal props after unmount/remount cycle", () => {
65122
const counter = signal(0);
66123
const visible = computed(() => counter.value >= 1 && counter.value <= 2);

packages/react/utils/src/index.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212

1313
interface ShowProps<T = boolean> {
1414
when: Signal<T> | ReadonlySignal<T> | (() => T);
15-
fallback?: ReactNode;
15+
fallback?: ReactNode | (() => ReactNode);
1616
children: ReactNode | ((value: NonNullable<T>) => ReactNode);
1717
}
1818

@@ -29,7 +29,12 @@ export function Show<T = boolean>(props: ShowProps<T>): JSX.Element | null {
2929
useSignals();
3030
const value =
3131
typeof props.when === "function" ? props.when() : props.when.value;
32-
if (!value) return (props.fallback as JSX.Element) || null;
32+
if (!value) {
33+
const fallback = props.fallback;
34+
return (
35+
typeof fallback === "function" ? fallback() : fallback
36+
) as JSX.Element | null;
37+
}
3338
return <Item v={value} children={props.children} />;
3439
}
3540

packages/react/utils/test/browser/index.test.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,61 @@ describe("@preact/signals-react-utils", () => {
118118
expect(scratch.innerHTML).to.eq("<p>Showing</p>");
119119
});
120120

121+
it("Should call function fallback lazily", async () => {
122+
const toggle = signal(true)!;
123+
let fallbackCalled = false;
124+
125+
await act(() => {
126+
render(
127+
<Show
128+
when={toggle}
129+
fallback={() => {
130+
fallbackCalled = true;
131+
return <p>Hidden</p>;
132+
}}
133+
>
134+
<p>Shown</p>
135+
</Show>
136+
);
137+
});
138+
139+
// When condition is true, fallback should NOT have been called
140+
expect(fallbackCalled).to.eq(false);
141+
expect(scratch.innerHTML).to.eq("<p>Shown</p>");
142+
143+
await act(() => {
144+
toggle.value = false;
145+
});
146+
147+
// Now fallback should have been called
148+
expect(fallbackCalled).to.eq(true);
149+
expect(scratch.innerHTML).to.eq("<p>Hidden</p>");
150+
});
151+
152+
it("Should reactively show with lazy function fallback", async () => {
153+
const toggle = signal(false)!;
154+
const Paragraph = (p: any) => <p>{p.children}</p>;
155+
156+
await act(() => {
157+
render(
158+
<Show when={toggle} fallback={() => <Paragraph>Hiding</Paragraph>}>
159+
<Paragraph>Showing</Paragraph>
160+
</Show>
161+
);
162+
});
163+
expect(scratch.innerHTML).to.eq("<p>Hiding</p>");
164+
165+
await act(() => {
166+
toggle.value = true;
167+
});
168+
expect(scratch.innerHTML).to.eq("<p>Showing</p>");
169+
170+
await act(() => {
171+
toggle.value = false;
172+
});
173+
expect(scratch.innerHTML).to.eq("<p>Hiding</p>");
174+
});
175+
121176
it("Should reactively show an inline element w/ nested reactivity", async () => {
122177
const count = signal(0);
123178
const visible = computed(() => count.value > 0)!;

0 commit comments

Comments
 (0)