From 7f8452758b645d075cf12f51c4bbb30bed665594 Mon Sep 17 00:00:00 2001 From: Joachim Viide Date: Mon, 15 Sep 2025 16:04:55 +0000 Subject: [PATCH] preact/signals: Make useComputed use the initial compute function forever --- .changeset/wise-coins-drop.md | 5 +++ packages/preact/src/index.ts | 9 ++--- packages/preact/test/index.test.tsx | 55 ++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 .changeset/wise-coins-drop.md diff --git a/.changeset/wise-coins-drop.md b/.changeset/wise-coins-drop.md new file mode 100644 index 000000000..329a13e83 --- /dev/null +++ b/.changeset/wise-coins-drop.md @@ -0,0 +1,5 @@ +--- +"@preact/signals": patch +--- + +Make useComputed always use the initial compute function diff --git a/packages/preact/src/index.ts b/packages/preact/src/index.ts index 7ffe05199..9ad18ff93 100644 --- a/packages/preact/src/index.ts +++ b/packages/preact/src/index.ts @@ -421,11 +421,12 @@ export function useSignal(value?: T, options?: SignalOptions) { )[0]; } -export function useComputed(compute: () => T, options?: SignalOptions) { - const $compute = useRef(compute); - $compute.current = compute; +export function useComputed( + compute: () => T, + options?: SignalOptions +): ReadonlySignal { (currentComponent as AugmentedComponent)._updateFlags |= HAS_COMPUTEDS; - return useMemo(() => computed(() => $compute.current(), options), []); + return useState(() => computed(compute, options))[0]; } function safeRaf(callback: () => void) { diff --git a/packages/preact/test/index.test.tsx b/packages/preact/test/index.test.tsx index 7d3ebe95b..b09861638 100644 --- a/packages/preact/test/index.test.tsx +++ b/packages/preact/test/index.test.tsx @@ -15,7 +15,13 @@ import { Component, } from "preact"; import type { ComponentChildren, FunctionComponent, VNode } from "preact"; -import { useContext, useEffect, useRef, useState } from "preact/hooks"; +import { + useContext, + useEffect, + useRef, + useState, + useCallback, +} from "preact/hooks"; import { setupRerender, act } from "preact/test-utils"; const sleep = (ms?: number) => new Promise(r => setTimeout(r, ms)); @@ -1001,4 +1007,51 @@ describe("@preact/signals", () => { expect(spy).to.have.been.calledWith("willmount:1"); }); }); + + describe("useComputed", () => { + it("should keep using the compute initial compute function", async () => { + const s1 = signal(1); + const s2 = signal("a"); + + function App({ x }: { x: Signal }) { + const c = useComputed(() => { + return x.value; + }); + return {c.value}; + } + + render(, scratch); + expect(scratch.textContent).to.equal("1"); + + render(, scratch); + expect(scratch.textContent).to.equal("1"); + + s1.value = 2; + rerender(); + expect(scratch.textContent).to.equal("2"); + }); + + it("should not recompute when the compute function doesn't change and dependency values don't change", async () => { + const s1 = signal(1); + const spy = sinon.spy(); + + function App({ x }: { x: Signal }) { + const fn = useCallback(() => { + spy(); + return x.value; + }, [x]); + + const c = useComputed(fn); + return {c.value}; + } + + render(, scratch); + expect(scratch.textContent).to.equal("1"); + expect(spy).to.have.been.calledOnce; + + rerender(); + expect(scratch.textContent).to.equal("1"); + expect(spy).to.have.been.calledOnce; + }); + }); });