diff --git a/.changeset/perfect-dolls-clean.md b/.changeset/perfect-dolls-clean.md new file mode 100644 index 000000000..404c8ec96 --- /dev/null +++ b/.changeset/perfect-dolls-clean.md @@ -0,0 +1,5 @@ +--- +"@preact/signals": patch +--- + +Update useComputed compute function on rerender diff --git a/packages/preact/src/index.ts b/packages/preact/src/index.ts index 7ffe05199..219cf42ad 100644 --- a/packages/preact/src/index.ts +++ b/packages/preact/src/index.ts @@ -421,11 +421,18 @@ 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 { + const [$fn, $computed] = useMemo(() => { + const $fn = signal(compute); + return [$fn, computed(() => $fn.value(), options)] as const; + }, []); + (currentComponent as AugmentedComponent)._updateFlags |= HAS_COMPUTEDS; - return useMemo(() => computed(() => $compute.current(), options), []); + $fn.value = compute; + return $computed; } function safeRaf(callback: () => void) { diff --git a/packages/preact/test/index.test.tsx b/packages/preact/test/index.test.tsx index 7d3ebe95b..fec3c928c 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,57 @@ describe("@preact/signals", () => { expect(spy).to.have.been.calledWith("willmount:1"); }); }); + + describe("useComputed", () => { + it("should recompute and update dependency list when the compute function changes", async () => { + const s1 = signal(1); + const s2 = signal("a"); + + function App({ x }: { x: Signal }) { + const fn = useCallback(() => { + return x.value; + }, [x]); + + const c = useComputed(fn); + return {c.value}; + } + + render(, scratch); + expect(scratch.textContent).to.equal("1"); + + render(, scratch); + expect(scratch.textContent).to.equal("a"); + + s1.value = 2; + rerender(); + expect(scratch.textContent).to.equal("a"); + + s2.value = "b"; + rerender(); + expect(scratch.textContent).to.equal("b"); + }); + + 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; + }); + }); });