Which @angular/* package(s) are relevant/related to the feature request?
core
Description
Currently, Angular provides a robust Signal API, but lacks a primitive to explicitly intercept and control both the get (dependency tracking) and set (update triggering) operations of a WritableSignal while preserving its standard interface.
This makes it difficult to model transactional updates between reactive state and external APIs (Storage, DOM, timers etc.). Relying on effect for synchronization is often insufficient because its asynchronous, coalescing nature cannot guarantee atomic consistency between state and external side effects.
Proposed solution
Introduce a new primitive, such as customSignal or proxySignal, that allows developers to wrap an existing signal or define a new one with intercepted get and set handlers.
Examples (pseudocode)
Persistent signal (external side effects):
function storageSignal<T>(key: string, initialValue: T): WritableSignal<T> {
const source = signal<T>(initialValue);
return proxySignal(source, {
set: (value, node) => {
/**
* Ensures a synchronous transaction in which state and storage are updated atomically.
* This guarantees deterministic side effects, including immediate cross-tab `storage` event dispatch,
* and avoids scheduling delays and value coalescing inherent to `effect()`.
*/
localStorage.setItem(key, JSON.stringify(value));
node.set(value);
}
});
}
Additionally, this approach allows us to ensure consistency between the signal state and external side effects:
try {
// localStorage can throw QuotaExceededError
localStorage.setItem(key, JSON.stringify(value));
// the signal is updated only if the write operation succeeds.
node.set(value);
} catch {
/* ... */
}
Throttled signal (custom update scheduling):
function throttledSignal<T>(initialValue: T, delay = 200) {
let lastEmit = 0;
const source = signal(initialValue);
return proxySignal(source, {
set: (value, node) => {
const wait = delay - (Date.now() - lastEmit);
const emit = () => { lastEmit = Date.now(); node.set(value); };
// Custom update scheduler:
wait <= 0 ? emit() : setTimeout(emit, wait);
}
});
}
Fallback signal:
function routeQuerySignal<T>(
key: string,
defaultValue?: T
) {
const source = signal<T | undefined>(undefined);
return proxySignal(source, {
get: (node) => {
const value = node();
return value !== undefined ? value : defaultValue;
},
});
}
Design considerations
-
While get interception enables control over dependency tracking, there are almost no compelling real-world use cases requiring such fine-grained control. It is primarily included for API symmetry and typically delegates directly to the underlying tracking mechanism. However, inspired by patterns such as Vue’s customRef, some libraries use it to either transform values or provide fallback values during read operations.
-
At this stage, the proposal focuses on the conceptual idea with illustrative examples, without defining a concrete API shape. The goal is to discuss the potential of this primitive with the community and contributors, and better understand its demand. In most cases, such functionality is primarily relevant for libraries built on top of signals.
Alternatives considered
- Using
effect(), which is asynchronous. It cannot intercept or modify the value before it is written to the signal. Additionally, we sometimes have to create a consumer that unnecessarily shows up in the dev tools.
- Creating custom wrapper objects/classes (e.g.,
{ value: Signal, set: (v) => void }), which breaks the WritableSignal API signature.
- Building custom primitives to manually proxy read/write.
Which @angular/* package(s) are relevant/related to the feature request?
core
Description
Currently, Angular provides a robust Signal API, but lacks a primitive to explicitly intercept and control both the
get(dependency tracking) andset(update triggering) operations of a WritableSignal while preserving its standard interface.This makes it difficult to model transactional updates between reactive state and external APIs (Storage, DOM, timers etc.). Relying on
effectfor synchronization is often insufficient because its asynchronous, coalescing nature cannot guarantee atomic consistency between state and external side effects.Proposed solution
Introduce a new primitive, such as
customSignalorproxySignal, that allows developers to wrap an existing signal or define a new one with interceptedgetandsethandlers.Examples (pseudocode)
Persistent signal (external side effects):
Additionally, this approach allows us to ensure consistency between the signal state and external side effects:
Throttled signal (custom update scheduling):
Fallback signal:
Design considerations
While
getinterception enables control over dependency tracking, there are almost no compelling real-world use cases requiring such fine-grained control. It is primarily included for API symmetry and typically delegates directly to the underlying tracking mechanism. However, inspired by patterns such as Vue’scustomRef, some libraries use it to either transform values or provide fallback values during read operations.At this stage, the proposal focuses on the conceptual idea with illustrative examples, without defining a concrete API shape. The goal is to discuss the potential of this primitive with the community and contributors, and better understand its demand. In most cases, such functionality is primarily relevant for libraries built on top of signals.
Alternatives considered
effect(), which is asynchronous. It cannot intercept or modify the value before it is written to the signal. Additionally, we sometimes have to create a consumer that unnecessarily shows up in the dev tools.{ value: Signal, set: (v) => void }), which breaks the WritableSignal API signature.