Skip to content

Commit

Permalink
Adds untracked.
Browse files Browse the repository at this point in the history
This allows signal reads which are not tracked by any active consumers/effects/etc.

```typescript
const foo = signal('foo');
const bar = signal('bar');

// Re-runs when `foo` changes, but not when `bar` changes because it is untracked.
host.effect(() => {
  const f = foo();
  const b = untracked(() => bar());
  console.log(`${f} - ${b}`);
});
```
  • Loading branch information
dgp1130 committed Dec 1, 2024
1 parent a772872 commit 8b93477
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 1 deletion.
58 changes: 57 additions & 1 deletion src/signals/graph.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { bindProducer, Consumer, observe, Producer } from './graph.js';
import { bindProducer, Consumer, observe, Producer, untracked } from './graph.js';
import { signal } from './signal.js';

describe('graph', () => {
Expand Down Expand Up @@ -73,6 +73,62 @@ describe('graph', () => {
});
});

describe('untracked', () => {
it('ignores producer reads inside the callback', () => {
const consumer = Consumer.from();

const listener = jasmine.createSpy<() => void>('listener');
consumer.listen(listener);

const foo = signal('foo');
const bar = signal('bar');
consumer.record(() => `${untracked(() => foo())} - ${bar()}`);

expect(listener).not.toHaveBeenCalled();
foo.set('foo2');
expect(listener).not.toHaveBeenCalled();
bar.set('bar2');
expect(listener).toHaveBeenCalledTimes(1);

consumer.destroy();
});

it('propagates errors', () => {
const err = new Error('Untrack this!');

expect(() => untracked(() => { throw err; })).toThrow(err);
});

it('resets the consumer on error', () => {
const consumer = Consumer.from();

const listener = jasmine.createSpy<() => void>('listener');
consumer.listen(listener);

const foo = signal('foo');
const bar = signal('bar');
consumer.record(() => {
try {
untracked(() => {
foo(); // `foo` still untracked.
throw new Error('Untrack this!');
});
} catch {}

// `bar` should not be affected by above error.
return bar();
});

expect(listener).not.toHaveBeenCalled();
foo.set('foo2');
expect(listener).not.toHaveBeenCalled();
bar.set('bar2');
expect(listener).toHaveBeenCalledTimes(1);

consumer.destroy();
});
});

describe('Producer', () => {
describe('poll', () => {
it('invokes the constructor parameter', () => {
Expand Down
30 changes: 30 additions & 0 deletions src/signals/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,36 @@ export function observe<Value>(consumer: Consumer, cb: () => Value): Value {
}
}

/**
* Immediately invokes the given callback in an "untracked" state, meaning any
* currently active {@link Consumer} objects will *not* observe signal reads.
*
* For example, the following effect will rerun whenever `foo` changes, but will
* *not* rerun when `bar` changes because it is untracked.
*
* ```typescript
* const foo = signal('foo');
* const bar = signal('bar');
*
* host.effect(() => {
* const f = foo();
* const b = untracked(() => bar());
* console.log(`${f} - ${b}`);
* });
* ```
*
* @param cb The callback to invoke in an untracked state.
* @returns The result of the callback.
*/
export function untracked<Value>(cb: () => Value): Value {
const stack = consumerStack.splice(0, consumerStack.length);
try {
return cb();
} finally {
consumerStack.push(...stack);
}
}

/**
* Binds the provided {@link Producer} to the currently observing
* {@link Consumer}, if present. This will configure the {@link Producer} to
Expand Down
1 change: 1 addition & 0 deletions src/signals/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { cached } from './cached.js';
export { effect } from './effect.js';
export { untracked } from './graph.js';
export { type ReactiveRoot } from './reactive-root.js';
export { type Equals, signal } from './signal.js';
export { MacrotaskScheduler } from './schedulers/macrotask-scheduler.js';
Expand Down

0 comments on commit 8b93477

Please sign in to comment.