From 256a331b5335e54f7e918b3f1068fb9d92d1c613 Mon Sep 17 00:00:00 2001 From: Valery Smirnov <57757211+XantreGodlike@users.noreply.github.com> Date: Wed, 26 Jul 2023 09:30:15 +0300 Subject: [PATCH] Added `untracked` function (#380) * Added untrack function * fixed naming * add tests * Fix tests * reexported untracked * fixed untracked * improved untracked tests * removed useless test * added docs * changeset * Update .changeset/dirty-geese-learn.md Co-authored-by: Jovi De Croock --------- Co-authored-by: Jovi De Croock --- .changeset/dirty-geese-learn.md | 5 +++ README.md | 20 +++++++++++ packages/core/src/index.ts | 27 ++++++++++++++- packages/core/test/signal.test.tsx | 55 +++++++++++++++++++++++++++++- packages/preact/README.md | 1 + packages/preact/src/index.ts | 11 +++++- packages/react/README.md | 1 + packages/react/src/index.ts | 2 ++ 8 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 .changeset/dirty-geese-learn.md diff --git a/.changeset/dirty-geese-learn.md b/.changeset/dirty-geese-learn.md new file mode 100644 index 000000000..51e130620 --- /dev/null +++ b/.changeset/dirty-geese-learn.md @@ -0,0 +1,5 @@ +--- +"@preact/signals-core": minor +--- + +Add `untracked` function, this allows more granular control within `effect`/`computed` around what should affect re-runs. diff --git a/README.md b/README.md index 706998fa0..405aeb7f5 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ npm install @preact/signals-core - [`computed(fn)`](#computedfn) - [`effect(fn)`](#effectfn) - [`batch(fn)`](#batchfn) + - [`untracked(fn)`](#untrackedfn) - [Preact Integration](./packages/preact/README.md#preact-integration) - [Hooks](./packages/preact/README.md#hooks) - [Rendering optimizations](./packages/preact/README.md#rendering-optimizations) @@ -75,6 +76,25 @@ effect(() => { Note that you should only use `signal.peek()` if you really need it. Reading a signal's value via `signal.value` is the preferred way in most scenarios. +### `untracked(fn)` + +In case when you're receiving a callback that can read some signals, but you don't want to subscribe to them, you can use `untracked` to prevent any subscriptions from happening. + +```js +const counter = signal(0); +const effectCount = signal(0); +const fn = () => effectCount.value + 1; + +effect(() => { + console.log(counter.value); + + // Whenever this effect is triggered, run `fn` that gives new value + effectCount.value = untracked(fn); +}); +``` + +Note that you should only use `signal.peek()` if you really need it. Reading a signal's value via `signal.value` is the preferred way in most scenarios. + ### `computed(fn)` Data is often derived from other pieces of existing data. The `computed` function lets you combine the values of multiple signals into a new signal that can be reacted to, or even used by additional computeds. When the signals accessed from within a computed callback change, the computed callback is re-executed and its new return value becomes the computed signal's value. diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4dacaf374..10a49efc5 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -97,6 +97,23 @@ function batch(callback: () => T): T { // Currently evaluated computed or effect. let evalContext: Computed | Effect | undefined = undefined; +let untrackedDepth = 0; + +function untracked(callback: () => T): T { + if (untrackedDepth > 0) { + return callback(); + } + const prevContext = evalContext; + evalContext = undefined; + untrackedDepth++; + try { + return callback(); + } finally { + untrackedDepth--; + evalContext = prevContext; + } +} + // Effects collected into a batch. let batchedEffect: Effect | undefined = undefined; let batchDepth = 0; @@ -752,4 +769,12 @@ function effect(compute: () => unknown | EffectCleanup): () => void { return effect._dispose.bind(effect); } -export { signal, computed, effect, batch, Signal, type ReadonlySignal }; +export { + signal, + computed, + effect, + batch, + Signal, + type ReadonlySignal, + untracked, +}; diff --git a/packages/core/test/signal.test.tsx b/packages/core/test/signal.test.tsx index 04aa12715..b467c8583 100644 --- a/packages/core/test/signal.test.tsx +++ b/packages/core/test/signal.test.tsx @@ -1,4 +1,11 @@ -import { signal, computed, effect, batch, Signal } from "@preact/signals-core"; +import { + signal, + computed, + effect, + batch, + Signal, + untracked, +} from "@preact/signals-core"; describe("signal", () => { it("should return value", () => { @@ -642,6 +649,36 @@ describe("effect()", () => { expect(spy).not.to.be.called; }); + it("should not run if readed signals in a untracked", () => { + const a = signal(1); + const b = signal(2); + const spy = sinon.spy(() => a.value + b.value); + effect(() => untracked(spy)); + a.value = 10; + b.value = 20; + + expect(spy).to.be.calledOnce; + }); + + it("should not throw on assignment in untracked", () => { + const a = signal(1); + const aChangedTime = signal(0); + + const dispose = effect(() => { + a.value; + untracked(() => { + aChangedTime.value = aChangedTime.value + 1; + }); + }); + + expect(() => (a.value = 2)).not.to.throw(); + expect(aChangedTime.value).to.equal(2); + a.value = 3; + expect(aChangedTime.value).to.equal(3); + + dispose(); + }); + it("should not rerun parent effect if a nested child effect's signal's value changes", () => { const parentSignal = signal(0); const childSignal = signal(0); @@ -948,6 +985,22 @@ describe("computed()", () => { expect(spy).to.be.calledTwice; }); + it("should not recompute if readed signals in a untracked", () => { + const a = signal(1); + const b = signal(2); + const spy = sinon.spy(() => a.value + b.value); + const c = computed(() => untracked(spy)); + + expect(spy).to.not.be.called; + expect(c.value).to.equal(3); + a.value = 10; + c.value; + b.value = 20; + c.value; + expect(spy).to.be.calledOnce; + expect(c.value).to.equal(3); + }); + it("should store thrown non-errors and recompute only after a dependency changes", () => { const a = signal(0); const spy = sinon.spy(); diff --git a/packages/preact/README.md b/packages/preact/README.md index 9fbe4d51b..2de3cf174 100644 --- a/packages/preact/README.md +++ b/packages/preact/README.md @@ -19,6 +19,7 @@ npm install @preact/signals - [`computed(fn)`](../../README.md#computedfn) - [`effect(fn)`](../../README.md#effectfn) - [`batch(fn)`](../../README.md#batchfn) + - [`untracked(fn)`](../../README.md#untrackedfn) - [Preact Integration](#preact-integration) - [Hooks](#hooks) - [Rendering optimizations](#rendering-optimizations) diff --git a/packages/preact/src/index.ts b/packages/preact/src/index.ts index f2968c0c5..f9c9b6e5f 100644 --- a/packages/preact/src/index.ts +++ b/packages/preact/src/index.ts @@ -7,6 +7,7 @@ import { effect, Signal, type ReadonlySignal, + untracked, } from "@preact/signals-core"; import { VNode, @@ -18,7 +19,15 @@ import { AugmentedElement as Element, } from "./internal"; -export { signal, computed, batch, effect, Signal, type ReadonlySignal }; +export { + signal, + computed, + batch, + effect, + Signal, + type ReadonlySignal, + untracked, +}; const HAS_PENDING_UPDATE = 1 << 0; const HAS_HOOK_STATE = 1 << 1; diff --git a/packages/react/README.md b/packages/react/README.md index d5c2f4480..c9ad47912 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -19,6 +19,7 @@ npm install @preact/signals-react - [`computed(fn)`](../../README.md#computedfn) - [`effect(fn)`](../../README.md#effectfn) - [`batch(fn)`](../../README.md#batchfn) + - [`untracked(fn)`](../../README.md#untrackedfn) - [React Integration](#react-integration) - [Hooks](#hooks) - [License](#license) diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 1ad99bc05..b2c03f140 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -5,6 +5,7 @@ import { effect, Signal, type ReadonlySignal, + untracked, } from "@preact/signals-core"; import type { ReactElement } from "react"; import { useSignal, useComputed, useSignalEffect } from "../runtime"; @@ -20,6 +21,7 @@ export { useSignal, useComputed, useSignalEffect, + untracked, }; declare module "@preact/signals-core" {