Skip to content

Commit

Permalink
Added untracked function (#380)
Browse files Browse the repository at this point in the history
* 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 <decroockjovi@gmail.com>

---------

Co-authored-by: Jovi De Croock <decroockjovi@gmail.com>
  • Loading branch information
XantreDev and JoviDeCroock authored Jul 26, 2023
1 parent 3b32463 commit 256a331
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/dirty-geese-learn.md
Original file line number Diff line number Diff line change
@@ -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.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
27 changes: 26 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,23 @@ function batch<T>(callback: () => T): T {
// Currently evaluated computed or effect.
let evalContext: Computed | Effect | undefined = undefined;

let untrackedDepth = 0;

function untracked<T>(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;
Expand Down Expand Up @@ -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,
};
55 changes: 54 additions & 1 deletion packages/core/test/signal.test.tsx
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions packages/preact/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 10 additions & 1 deletion packages/preact/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
effect,
Signal,
type ReadonlySignal,
untracked,
} from "@preact/signals-core";
import {
VNode,
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -20,6 +21,7 @@ export {
useSignal,
useComputed,
useSignalEffect,
untracked,
};

declare module "@preact/signals-core" {
Expand Down

0 comments on commit 256a331

Please sign in to comment.