diff --git a/.changeset/chilled-yaks-occur.md b/.changeset/chilled-yaks-occur.md new file mode 100644 index 000000000..0b267e1a7 --- /dev/null +++ b/.changeset/chilled-yaks-occur.md @@ -0,0 +1,14 @@ +--- +"@preact/signals": minor +"@preact/signals-react": minor +--- + +Add support for passing functions as fallback to `` for lazy instantiation. + +```tsx +

I'm lazy

}> +

foo

+
+``` + +This avoids eager evaluation of whatever is passed to fallback which matters when you're dealing with signals. diff --git a/packages/preact/utils/src/index.tsx b/packages/preact/utils/src/index.tsx index 27dafe4ff..0dc5356db 100644 --- a/packages/preact/utils/src/index.tsx +++ b/packages/preact/utils/src/index.tsx @@ -5,7 +5,7 @@ import { useMemo } from "preact/hooks"; interface ShowProps { when: Signal | ReadonlySignal | (() => T); - fallback?: ComponentChildren; + fallback?: ComponentChildren | (() => ComponentChildren); children: ComponentChildren | ((value: NonNullable) => ComponentChildren); } @@ -22,7 +22,10 @@ export function Show( ): ComponentChildren | null { const value = typeof props.when === "function" ? props.when() : props.when.value; - if (!value) return props.fallback || null; + if (!value) { + const fallback = props.fallback; + return typeof fallback === "function" ? fallback() : fallback || null; + } return ; } diff --git a/packages/preact/utils/test/browser/index.test.tsx b/packages/preact/utils/test/browser/index.test.tsx index f7f5edc13..13e916f55 100644 --- a/packages/preact/utils/test/browser/index.test.tsx +++ b/packages/preact/utils/test/browser/index.test.tsx @@ -61,6 +61,63 @@ describe("@preact/signals-utils", () => { expect(scratch.innerHTML).to.eq("

Showing 2

"); }); + it("Should call function fallback lazily", () => { + const toggle = signal(true)!; + let fallbackCalled = false; + + act(() => { + render( + { + fallbackCalled = true; + return

Hidden

; + }} + > +

Shown

+
, + scratch + ); + }); + + // When condition is true, fallback should NOT have been called + expect(fallbackCalled).to.eq(false); + expect(scratch.innerHTML).to.eq("

Shown

"); + + act(() => { + toggle.value = false; + }); + + // Now fallback should have been called + expect(fallbackCalled).to.eq(true); + expect(scratch.innerHTML).to.eq("

Hidden

"); + }); + + it("Should reactively show with lazy function fallback", () => { + const toggle = signal(false)!; + const Paragraph = (props: any) =>

{props.children}

; + + act(() => { + render( + Hiding}> + Showing + , + scratch + ); + }); + expect(scratch.innerHTML).to.eq("

Hiding

"); + + act(() => { + toggle.value = true; + }); + expect(scratch.innerHTML).to.eq("

Showing

"); + + act(() => { + toggle.value = false; + }); + expect(scratch.innerHTML).to.eq("

Hiding

"); + }); + it("Should preserve signal props after unmount/remount cycle", () => { const counter = signal(0); const visible = computed(() => counter.value >= 1 && counter.value <= 2); diff --git a/packages/react/utils/src/index.tsx b/packages/react/utils/src/index.tsx index eb99ad5d0..8764e3dd9 100644 --- a/packages/react/utils/src/index.tsx +++ b/packages/react/utils/src/index.tsx @@ -12,7 +12,7 @@ import { interface ShowProps { when: Signal | ReadonlySignal | (() => T); - fallback?: ReactNode; + fallback?: ReactNode | (() => ReactNode); children: ReactNode | ((value: NonNullable) => ReactNode); } @@ -29,7 +29,12 @@ export function Show(props: ShowProps): JSX.Element | null { useSignals(); const value = typeof props.when === "function" ? props.when() : props.when.value; - if (!value) return (props.fallback as JSX.Element) || null; + if (!value) { + const fallback = props.fallback; + return ( + typeof fallback === "function" ? fallback() : fallback + ) as JSX.Element | null; + } return ; } diff --git a/packages/react/utils/test/browser/index.test.tsx b/packages/react/utils/test/browser/index.test.tsx index df6a25582..3c4554929 100644 --- a/packages/react/utils/test/browser/index.test.tsx +++ b/packages/react/utils/test/browser/index.test.tsx @@ -118,6 +118,61 @@ describe("@preact/signals-react-utils", () => { expect(scratch.innerHTML).to.eq("

Showing

"); }); + it("Should call function fallback lazily", async () => { + const toggle = signal(true)!; + let fallbackCalled = false; + + await act(() => { + render( + { + fallbackCalled = true; + return

Hidden

; + }} + > +

Shown

+
+ ); + }); + + // When condition is true, fallback should NOT have been called + expect(fallbackCalled).to.eq(false); + expect(scratch.innerHTML).to.eq("

Shown

"); + + await act(() => { + toggle.value = false; + }); + + // Now fallback should have been called + expect(fallbackCalled).to.eq(true); + expect(scratch.innerHTML).to.eq("

Hidden

"); + }); + + it("Should reactively show with lazy function fallback", async () => { + const toggle = signal(false)!; + const Paragraph = (p: any) =>

{p.children}

; + + await act(() => { + render( + Hiding}> + Showing + + ); + }); + expect(scratch.innerHTML).to.eq("

Hiding

"); + + await act(() => { + toggle.value = true; + }); + expect(scratch.innerHTML).to.eq("

Showing

"); + + await act(() => { + toggle.value = false; + }); + expect(scratch.innerHTML).to.eq("

Hiding

"); + }); + it("Should reactively show an inline element w/ nested reactivity", async () => { const count = signal(0); const visible = computed(() => count.value > 0)!;