feat(core): add hook for producer creation side effects (#60333)

Adds a hook in the same style as `postSignalSetFn` for running side effects when a producer has been created. This hook will be passed the reactive node being created.

PR Close #60333
This commit is contained in:
Matt Turco
2025-03-11 11:54:02 -06:00
committed by Pawel Kozlowski
parent 69b688c7d1
commit 22d3f0562c
9 changed files with 99 additions and 11 deletions
@@ -20,6 +20,7 @@ export {setThrowInvalidWriteToSignalError} from './src/errors';
export {
REACTIVE_NODE,
Reactive,
ReactiveHookFn,
ReactiveNode,
SIGNAL,
consumerAfterComputation,
@@ -36,7 +37,9 @@ export {
producerNotifyConsumers,
producerUpdateValueVersion,
producerUpdatesAllowed,
runPostProducerCreatedFn,
setActiveConsumer,
setPostProducerCreatedFn,
} from './src/graph';
export {
SIGNAL_NODE,
@@ -16,6 +16,7 @@ import {
ReactiveNode,
setActiveConsumer,
SIGNAL,
runPostProducerCreatedFn,
} from './graph';
/**
@@ -76,6 +77,7 @@ export function createComputed<T>(
return node.value;
};
(computed as ComputedGetter<T>)[SIGNAL] = node;
runPostProducerCreatedFn(node);
return computed as unknown as ComputedGetter<T>;
}
@@ -25,6 +25,13 @@ type Version = number & {__brand: 'Version'};
*/
let epoch: Version = 1 as Version;
export type ReactiveHookFn = (node: ReactiveNode) => void;
/**
* If set, called after a producer `ReactiveNode` is created.
*/
let postProducerCreatedFn: ReactiveHookFn | null = null;
/**
* Symbol used to tell `Signal`s apart from other functions.
*
@@ -527,3 +534,13 @@ function assertProducerNode(node: ReactiveNode): asserts node is ProducerNode {
function isConsumerNode(node: ReactiveNode): node is ConsumerNode {
return node.producerNode !== undefined;
}
export function runPostProducerCreatedFn(node: ReactiveNode): void {
postProducerCreatedFn?.(node);
}
export function setPostProducerCreatedFn(fn: ReactiveHookFn | null): ReactiveHookFn | null {
const prev = postProducerCreatedFn;
postProducerCreatedFn = fn;
return prev;
}
@@ -16,6 +16,7 @@ import {
producerUpdateValueVersion,
REACTIVE_NODE,
ReactiveNode,
runPostProducerCreatedFn,
SIGNAL,
} from './graph';
import {signalSetFn, signalUpdateFn} from './signal';
@@ -86,7 +87,7 @@ export function createLinkedSignal<S, D>(
const getter = linkedSignalGetter as LinkedSignalGetter<S, D>;
getter[SIGNAL] = node;
runPostProducerCreatedFn(node);
return getter;
}
@@ -15,6 +15,8 @@ import {
producerUpdatesAllowed,
REACTIVE_NODE,
ReactiveNode,
ReactiveHookFn,
runPostProducerCreatedFn,
SIGNAL,
} from './graph';
@@ -28,7 +30,7 @@ declare const ngDevMode: boolean | undefined;
* This hook can be used to achieve various effects, such as running effects synchronously as part
* of setting a signal.
*/
let postSignalSetFn: (() => void) | null = null;
let postSignalSetFn: ReactiveHookFn | null = null;
export interface SignalNode<T> extends ReactiveNode {
value: T;
@@ -57,10 +59,11 @@ export function createSignal<T>(initialValue: T, equal?: ValueEqualityFn<T>): Si
return node.value;
}) as SignalGetter<T>;
(getter as any)[SIGNAL] = node;
runPostProducerCreatedFn(node);
return getter;
}
export function setPostSignalSetFn(fn: (() => void) | null): (() => void) | null {
export function setPostSignalSetFn(fn: ReactiveHookFn | null): ReactiveHookFn | null {
const prev = postSignalSetFn;
postSignalSetFn = fn;
return prev;
@@ -90,8 +93,8 @@ export function signalUpdateFn<T>(node: SignalNode<T>, updater: (value: T) => T)
signalSetFn(node, updater(node.value));
}
export function runPostSignalSetFn(): void {
postSignalSetFn?.();
export function runPostSignalSetFn<T>(node: SignalNode<T>): void {
postSignalSetFn?.(node);
}
// Note: Using an IIFE here to ensure that the spread assignment is not considered
@@ -110,5 +113,5 @@ function signalValueChanged<T>(node: SignalNode<T>): void {
node.version++;
producerIncrementEpoch();
producerNotifyConsumers(node);
postSignalSetFn?.();
postSignalSetFn?.(node);
}
+17 -1
View File
@@ -7,7 +7,13 @@
*/
import {computed, signal} from '@angular/core';
import {createWatch, ReactiveNode, SIGNAL, defaultEquals} from '@angular/core/primitives/signals';
import {
createWatch,
ReactiveNode,
SIGNAL,
defaultEquals,
setPostProducerCreatedFn,
} from '@angular/core/primitives/signals';
describe('computed', () => {
it('should create computed', () => {
@@ -317,4 +323,14 @@ describe('computed', () => {
expect(derived()).toBe(2);
});
});
it('should call the post-producer-created fn when signal is called', () => {
const producerKindsCreated: string[] = [];
const prev = setPostProducerCreatedFn((node) => producerKindsCreated.push(node.kind));
const count = signal(0);
computed(() => count() % 2 === 0);
expect(producerKindsCreated).toEqual(['signal', 'computed']);
setPostProducerCreatedFn(prev);
});
});
@@ -7,6 +7,7 @@
*/
import {isSignal, linkedSignal, signal, computed} from '@angular/core';
import {setPostProducerCreatedFn} from '@angular/core/primitives/signals';
import {testingEffect} from './effect_util';
describe('linkedSignal', () => {
@@ -275,4 +276,14 @@ describe('linkedSignal', () => {
choice.set('explicit');
expect(choice()).toBe('explicit');
});
it('should call the post-producer-created fn when signal is called', () => {
let producers = 0;
const prev = setPostProducerCreatedFn(() => producers++);
const options = signal(['apple', 'banana', 'fig']);
linkedSignal(() => options()[0]);
expect(producers).toBe(2);
setPostProducerCreatedFn(prev);
});
});
+28 -2
View File
@@ -7,7 +7,13 @@
*/
import {computed, signal} from '@angular/core';
import {ReactiveNode, setPostSignalSetFn, SIGNAL} from '@angular/core/primitives/signals';
import {
ReactiveHookFn,
ReactiveNode,
setPostProducerCreatedFn,
setPostSignalSetFn,
SIGNAL,
} from '@angular/core/primitives/signals';
describe('signals', () => {
it('should be a getter which reflects the set value', () => {
@@ -166,7 +172,7 @@ describe('signals', () => {
});
describe('post-signal-set functions', () => {
let prevPostSignalSetFn: (() => void) | null = null;
let prevPostSignalSetFn: ReactiveHookFn | null = null;
let log: number;
beforeEach(() => {
log = 0;
@@ -197,5 +203,25 @@ describe('signals', () => {
counter.set(0);
expect(log).toBe(0);
});
it('should pass post-signal-set fn the node that was updated', () => {
const counter = signal(0, {debugName: 'test-signal'});
let node: ReactiveNode | null = null;
setPostSignalSetFn((n: ReactiveNode) => {
node = n;
});
counter.set(1);
expect(node!.debugName).toBe('test-signal');
});
});
it('should call the post-producer-created fn when signal is called', () => {
const producerKindsCreated: string[] = [];
const prev = setPostProducerCreatedFn((node) => producerKindsCreated.push(node.kind));
signal(0);
expect(producerKindsCreated).toEqual(['signal']);
setPostProducerCreatedFn(prev);
});
});