Angular 52 🅰️ Advanced Signal Patterns
The basic signal API — signal, computed, effect — covers the common cases. The advanced patterns cover the cases where the state is more complex: state that derives from an input and resets when the input changes, state that is written asynchronously, state that depends on a resource, state that is stored in a service and shared across components, and state that needs to be persisted. The linkedSignal and the resource APIs are the modern additions that make these patterns declarative, and they replace the older patterns that used an effect to write to a signal or a switchMap to fetch. This chapter covers the advanced patterns: the linkedSignal for the reset-on-change state, the resource for the async data, the signal-based store for the shared state, the computed chains for the derived values, the untracked for the reads that should not track, and the patterns that make the complex state predictable. It builds on the signal material from Angular 47 through 51 and treats the advanced patterns as the tools for the state that has more than one input.
Key point: The linkedSignal creates a writable signal whose value is derived from a source and resets when the source changes. The resource wraps an async operation and exposes the value, status, and error as signals. The signal-based store holds the state in signals and exposes the read-only views and the write methods. The computed chains derive the values from the base signals, and the chain is the graph. The untracked reads a signal without registering the dependency, and it is the tool for the reads that should not re-trigger the computation. The patterns compose: a linkedSignal that derives from a resource‘s value, a computed that combines several signals, a store that uses the linkedSignal for the local state.
The linkedSignal
The linkedSignal creates a writable signal whose value is derived from a source and resets when the source changes. It is the pattern for the state that has a default derived from the input but can be overridden by the user.
import { linkedSignal, input } from '@angular/core';
@Component({ selector: 'app-select', standalone: true, template: `` })
export class SelectComponent {
readonly options = input.required<string[]>();
readonly selected = linkedSignal(() => this.options()[0] ?? '');
}
The selected is derived from the options input. The initial value is the first option, and the value resets to the first option when the options changes. But the selected can be written by the user’s selection, which overrides the derivation until the options changes again.
Why the linkedSignal is the reset-on-change pattern. The pattern is common: a select with a default, a filter that resets when the data changes, a tab that resets when the context changes. The classic implementation was an effect that writes to a signal when the input changes, which is the wrong tool. The linkedSignal is the declarative version.
Why the linkedSignal is writable. The signal can be written by the component, and the write overrides the derivation. The next change to the source resets the value. The writable nature is the difference from the computed.
Why the linkedSignal can have a custom computation. The linkedSignal accepts a computation that receives the source and the previous value, and it returns the new value. The computation can decide whether to reset or to keep the previous value.
readonly selected = linkedSignal({
source: this.options,
computation: (options, previous) =>
options.includes(previous?.value) ? previous.value : options[0],
});
The computation receives the new options and the previous state, and it returns the new value. The pattern is for the cases where the reset should be conditional.
Why the linkedSignal is the alternative to the effect that writes. The pattern of an effect that writes to a signal when an input changes is the classic mistake — the effect is the wrong tool. The linkedSignal is the correct tool, and the reset is automatic.
Why the linkedSignal is the modern state pattern. The linkedSignal is the declarative way to express the “reset when the source changes, but allow the user to override” pattern. The pattern is common, and the linkedSignal is the tool.
Why the linkedSignal is not a computed. The computed is read-only, and the linkedSignal is writable. The computed recomputes on the source change, and the linkedSignal resets on the source change. The two are for the different cases.
Why the linkedSignal is not a plain signal. The plain signal has no relationship to the source, and the reset is manual. The linkedSignal has the relationship, and the reset is automatic. The relationship is the point.
Why the
linkedSignalis the newest of the signal APIs. ThelinkedSignalwas added in Angular 19 as the declarative answer to the “reset on change” pattern. The older code used theeffectthat writes, and thelinkedSignalreplaces it. The API is the modern, and the pattern is the common.
The resource API
The resource API wraps an async operation and exposes the value, status, and error as signals. It is the signal-based replacement for the switchMap-plus-toSignal pattern.
import { resource, signal } from '@angular/core';
@Component({
selector: 'app-users',
standalone: true,
template: `
@if (users.status() === 'loading') { <p>Loading...</p> }
@if (users.error()) { <p>Error: {{ users.error() }}</p> }
@if (users.value(); as list) {
@for (user of list; track user.id) { <div>{{ user.name }}</div> }
}
`,
})
export class UsersComponent {
readonly filter = signal('');
readonly users = resource({
request: () => ({ filter: this.filter() }),
loader: ({ request, abortSignal }) =>
fetch(`/api/users?q=${request.filter}`, { signal: abortSignal })
.then((r) => r.json() as Promise<User[]>),
});
}
The resource has a request function that produces the parameters, and a loader that performs the async operation. The signals — value, status, error — are the result.
Why the resource is the modern data fetching. The switchMap-plus-toSignal pattern was the previous approach: a signal to an Observable, a switchMap to the request, and a toSignal back. The resource is the single declaration, and the request, the loading state, and the error are all managed.
Why the resource has a request function. The request function produces the parameters for the loader, and it is tracked. When the request’s dependencies change, the loader re-runs with the new parameters, and the previous request is aborted.
Why the resource has an abortSignal. The abortSignal is passed to the loader and aborts the previous request when a new one starts. The pattern is the cancellation, and the resource provides the signal.
Why the resource exposes status. The status signal is 'idle', 'loading', 'error', 'success', or 'reloading'. The template uses the status to render the loading and error states. The status is the state machine, and the resource manages it.
Why the resource exposes error. The error signal is the error from the loader, and the template renders it. The error is the state, and the resource tracks it.
Why the resource exposes value. The value signal is the loader’s result, and the template reads it. The value is undefined until the loader completes, and the template handles the undefined.
Why the resource has a reload method. The reload method re-runs the loader with the current request. The method is for the manual refresh, and the pattern is the refresh button.
Why the resource is the modern pattern for the async data. The resource combines the request, the loading state, the error, the value, the cancellation, and the reload. The pattern is the same for every async data, and the resource is the single declaration.
The rxResource. The rxResource is the variant that uses an Observable as the loader, which integrates with the HttpClient.
readonly users = rxResource({
request: () => ({ filter: this.filter() }),
loader: ({ request }) => this.http.get<User[]>(`/api/users?q=${request.filter}`),
});
The rxResource is the RxJS-based variant, and the resource is the Promise-based. The two are the choice, and the rxResource is the common in Angular.
The signal-based store
The signal-based store holds the state in signals and exposes the read-only views and the write methods. It is the modern replacement for the BehaviorSubject-based store.
@Injectable({ providedIn: 'root' })
export class CartStore {
private readonly items = signal<CartItem[]>([]);
private readonly discount = signal(0);
readonly cartItems = this.items.asReadonly();
readonly subtotal = computed(() =>
this.items().reduce((sum, i) => sum + i.price * i.quantity, 0),
);
readonly total = computed(() => this.subtotal() - this.discount());
add(item: CartItem): void {
this.items.update((list) => [...list, item]);
}
remove(id: string): void {
this.items.update((list) => list.filter((i) => i.id !== id));
}
applyDiscount(amount: number): void {
this.discount.set(amount);
}
}
The store holds the items and the discount as private writable signals. The cartItems is the read-only view, and the subtotal and the total are the computed values. The methods are the writes.
Why the private writable signals. The state is the private writable, and the consumers see the read-only views. The encapsulation is what makes the state’s changes traceable to the store’s methods.
Why the asReadonly. The asReadonly method exposes the read-only view, and the consumers cannot write. The pattern is the same as the BehaviorSubject store’s asObservable.
Why the computed for the derived. The subtotal and the total are derived from the items and the discount. The computed caches the values and recomputes when the dependencies change.
Why the methods are the writes. The add, the remove, and the applyDiscount are the imperative writes. The store is the only writer, and the components call the methods.
Why the store is the singleton. The store is providedIn: 'root', and it is the singleton. The state is shared across the components, and the store is the single source.
Why the signal-based store is the modern. The signal-based store replaces the BehaviorSubject-based store. The signals are synchronous, the change detection is signal-driven, and the computed is the derivation. The store is simpler, and the model is the uniform.
Why the store can combine the signals and the streams. The store can hold a signal for the local state and a stream for the async state. The toSignal and the toObservable are the bridges, and the store is the connection.
The store with the resource. The store can use the resource for the async data and the signals for the local state.
@Injectable({ providedIn: 'root' })
export class UsersStore {
private readonly http = inject(HttpClient);
readonly filter = signal('');
readonly users = rxResource({
request: () => ({ filter: this.filter() }),
loader: ({ request }) => this.http.get<User[]>(`/api/users?q=${request.filter}`),
});
}
The users resource is the async data, and the filter is the local state. The store combines the two, and the components read the resource’s signals.
The computed chains
The computed chains derive the values from the base signals, and the chain is the graph. The pattern is the composition, and the graph is the derivation.
const items = signal<CartItem[]>([]);
const taxRate = signal(0.1);
const subtotal = computed(() =>
items().reduce((sum, i) => sum + i.price * i.quantity, 0),
);
const tax = computed(() => subtotal() * taxRate());
const shipping = computed(() => (subtotal() > 100 ? 0 : 10));
const total = computed(() => subtotal() + tax() + shipping());
The subtotal depends on the items. The tax, the shipping, and the total depend on the subtotal. The graph is the chain, and the propagation is the recomputation.
Why the chain is the composition. The complex derivation is built from the small pieces. Each computed does one thing, and the pieces compose. The chain is the derivation, and the individual computeds are the steps.
Why the chain is efficient. The change to the items marks the subtotal as stale, and the staleness propagates to the tax, the shipping, and the total. The recomputation is the lazy, and the memoization is the caching.
Why the chain can be deep. The graph can be as deep as the derivation requires. The chain of the ten computeds is the fine, and the propagation is the linear.
Why the chain should avoid the redundancy. The computed that recomputes the value that the another computed already computed is the waste. The shared value should be the computed that the both depend on. The subtotal in the example is the shared value.
Why the chain is testable. The each computed is the function that reads the signals and returns the value. The function can be tested in the isolation, with the signals set to the values. The chain is the graph, and the nodes are the tests.
Why the chain is the diamond. The diamond in the graph — the two paths from the source to the computed — is the fine. The total depends on the subtotal directly and through the tax and the shipping. The diamond is the graph, and the consistency is guaranteed.
Why the diamond is the glitch-free. The graph ensures that the total sees the subtotal, the tax, and the shipping at the same revision. The propagation is the topological, and the values are the consistent.
The untracked reads
The untracked function reads a signal without registering the dependency. It is the tool for the reads that should not re-trigger the computation.
import { computed, signal, untracked } from '@angular/core';
const a = signal(1);
const b = signal(2);
const sum = computed(() => {
const aValue = a();
const bValue = untracked(() => b()); // b is not a dependency
return aValue + bValue;
});
The sum depends on the a but not the b. The b‘s change does not re-trigger the computation, and the sum‘s value is the stale until the a changes.
Why the untracked is needed. The computed and the effect track the reads by default. The untracked is the way to read a signal without the tracking, which is the case for the reads that should not re-trigger the computation.
Why the untracked is the escape hatch. The tracking is the model’s strength, and the untracked is the exception. The function is for the cases where the read should not be the dependency, and the use should be the deliberate.
Why the untracked is used in the effect. The effect that reads a signal for the comparison but does not want to re-run on the change uses the untracked.
effect(() => {
const current = this.value();
const previous = untracked(() => this.previousValue());
console.log('changed from', previous, 'to', current);
});
The effect depends on the value but not the previousValue. The comparison uses the previous value, and the effect does not re-run when the previous value changes.
Why the untracked is used in the linkedSignal. The linkedSignal‘s computation can use the untracked to read the previous value without the tracking.
Why the untracked is the last resort. The untracked should be used only when the default tracking is wrong. The common case is the tracking, and the untracked is the exception. The overuse of the untracked is the loss of the model’s strength.
Why the untracked is the complement to the tracking. The tracking is the default, and the untracked is the escape. The two are the pair, and the choice is the deliberate.
The persistence pattern
The persistence pattern saves the state to the localStorage or the sessionStorage. The pattern is the effect that writes the state, and the initialization reads the stored value.
@Injectable({ providedIn: 'root' })
export class SettingsStore {
private readonly _settings = signal<Settings>(loadSettings());
readonly settings = this._settings.asReadonly();
constructor() {
effect(() => {
saveSettings(this._settings());
});
}
update(partial: Partial<Settings>): void {
this._settings.update((s) => ({ ...s, ...partial }));
}
}
function loadSettings(): Settings {
const stored = localStorage.getItem('settings');
return stored ? JSON.parse(stored) : { theme: 'light', language: 'en' };
}
function saveSettings(settings: Settings): void {
localStorage.setItem('settings', JSON.stringify(settings));
}
The _settings signal is initialized from the localStorage, and the effect saves the changes. The update method is the write, and the settings is the read-only view.
Why the initialization reads the storage. The signal’s initial value is the stored value or the default. The read is the synchronous, and the localStorage is the synchronous API.
Why the effect saves the changes. The effect runs when the _settings changes, and it saves the new value. The effect is the right tool for the persistence — it is the side effect, and the computed cannot express it.
Why the update method is the write. The update method is the imperative write, and the _settings is the private. The components call the update, and the store is the writer.
Why the persistence should be the deliberate. The persistence is the side effect, and the not every state should be persisted. The choice is the design, and the effect is the mechanism.
Why the localStorage is the synchronous. The localStorage is the synchronous API, and the read and the write are the immediate. The signal’s initial value can be the stored value, and the pattern is the simple.
Why the sessionStorage is the alternative. The sessionStorage is the per-tab storage, and the localStorage is the persistent. The choice is the scope, and the two are the similar.
Complete Example Session
import {
Component, Injectable, inject, signal, computed, effect,
linkedSignal, resource, rxResource, input, untracked,
} from '@angular/core';
import { HttpClient } from '@angular/common/http';
// ============================================
// PART 1: THE LINKED SIGNAL
// ============================================
@Component({ selector: 'app-select', standalone: true, template: `` })
export class SelectComponent {
readonly options = input.required<string[]>();
readonly selected = linkedSignal(() => this.options()[0] ?? '');
select(option: string): void {
this.selected.set(option);
}
}
// ============================================
// PART 2: THE LINKED SIGNAL WITH THE COMPUTATION
// ============================================
@Component({ selector: 'app-keep', standalone: true, template: `` })
export class KeepComponent {
readonly options = input.required<string[]>();
readonly selected = linkedSignal({
source: this.options,
computation: (options, previous) =>
options.includes(previous?.value) ? previous.value : options[0],
});
}
// ============================================
// PART 3: THE RESOURCE
// ============================================
@Component({ selector: 'app-users', standalone: true, template: `` })
export class UsersComponent {
readonly filter = signal('');
readonly users = resource({
request: () => ({ filter: this.filter() }),
loader: ({ request, abortSignal }) =>
fetch(`/api/users?q=${request.filter}`, { signal: abortSignal })
.then((r) => r.json() as Promise<User[]>),
});
}
// ============================================
// PART 4: THE RX RESOURCE
// ============================================
@Component({ selector: 'app-items', standalone: true, template: `` })
export class ItemsComponent {
private readonly http = inject(HttpClient);
readonly filter = signal('');
readonly items = rxResource({
request: () => ({ filter: this.filter() }),
loader: ({ request }) => this.http.get<Item[]>(`/api/items?q=${request.filter}`),
});
}
// ============================================
// PART 5: THE SIGNAL STORE
// ============================================
@Injectable({ providedIn: 'root' })
export class CartStore {
private readonly items = signal<CartItem[]>([]);
private readonly discount = signal(0);
readonly cartItems = this.items.asReadonly();
readonly subtotal = computed(() =>
this.items().reduce((sum, i) => sum + i.price * i.quantity, 0),
);
readonly total = computed(() => this.subtotal() - this.discount());
add(item: CartItem): void {
this.items.update((list) => [...list, item]);
}
remove(id: string): void {
this.items.update((list) => list.filter((i) => i.id !== id));
}
applyDiscount(amount: number): void {
this.discount.set(amount);
}
}
// ============================================
// PART 6: THE COMPUTED CHAIN
// ============================================
const items = signal<CartItem[]>([]);
const taxRate = signal(0.1);
const subtotal = computed(() =>
items().reduce((sum, i) => sum + i.price * i.quantity, 0),
);
const tax = computed(() => subtotal() * taxRate());
const shipping = computed(() => (subtotal() > 100 ? 0 : 10));
const total = computed(() => subtotal() + tax() + shipping());
// ============================================
// PART 7: THE UNTRACKED
// ============================================
const a = signal(1);
const b = signal(2);
const sum = computed(() => {
const aValue = a();
const bValue = untracked(() => b());
return aValue + bValue;
});
// ============================================
// PART 8: THE UNTRACKED IN THE EFFECT
// ============================================
@Component({ selector: 'app-compare', standalone: true, template: `` })
export class CompareComponent {
readonly value = signal(0);
readonly previous = signal(0);
constructor() {
effect(() => {
const current = this.value();
const prev = untracked(() => this.previous());
console.log('changed from', prev, 'to', current);
this.previous.set(current);
});
}
}
// ============================================
// PART 9: THE PERSISTENCE
// ============================================
interface Settings {
theme: 'light' | 'dark';
language: string;
}
@Injectable({ providedIn: 'root' })
export class SettingsStore {
private readonly _settings = signal<Settings>(loadSettings());
readonly settings = this._settings.asReadonly();
constructor() {
effect(() => saveSettings(this._settings()));
}
update(partial: Partial<Settings>): void {
this._settings.update((s) => ({ ...s, ...partial }));
}
}
function loadSettings(): Settings {
const stored = localStorage.getItem('settings');
return stored ? JSON.parse(stored) : { theme: 'light', language: 'en' };
}
function saveSettings(settings: Settings): void {
localStorage.setItem('settings', JSON.stringify(settings));
}
// ============================================
// PART 10: WHAT NOT TO DO
// ============================================
// Don't use an effect to write a derived value
// effect(() => this.doubled.set(this.count() * 2)); // use linkedSignal
// Don't use the switchMap + toSignal for the async data
// toSignal(toObservable(this.filter).pipe(switchMap(fetch))); // use resource
// Don't expose the writable signal
// readonly items = this.items; // use asReadonly
// Don't overuse the untracked
// The default is the tracking.
// Don't forget the linkedSignal's reset
// The value resets when the source changes.
// Don't use the plain signal for the reset-on-change state
// readonly selected = signal(''); // use linkedSignal
The ten parts cover the linkedSignal, the linkedSignal with the computation, the resource, the rxResource, the signal store, the computed chain, the untracked, the untracked in the effect, the persistence, and the anti-patterns.
Quick Reference
The Advanced APIs
| API | Purpose |
|---|---|
linkedSignal | The reset-on-change state |
resource | The Promise-based async data |
rxResource | The Observable-based async data |
untracked | The read without the tracking |
The linkedSignal
| Form | Purpose |
|---|---|
linkedSignal(() => source()) | The reset on the source change |
linkedSignal({ source, computation }) | The custom reset logic |
The resource
| Signal/Method | Purpose |
|---|---|
value() | The result |
status() | The idle, loading, error, success, reloading |
error() | The error |
reload() | The re-run |
The Signal Store
| Pattern | Code |
|---|---|
| Private state | private readonly items = signal([]) |
| Public view | readonly items = this._items.asReadonly() |
| Derived | readonly total = computed(() => ...) |
| Write | add(item: Item) { this.items.update(...) } |
The untracked
| Form | Purpose |
|---|---|
untracked(() => sig()) | The read without the tracking |
In the computed | The read that is not the dependency |
In the effect | The read that does not re-trigger |
The Persistence
| Part | Code |
|---|---|
| Load | signal(loadFromStorage()) |
| Save | effect(() => saveToStorage(this._state())) |
| Update | update(partial) { this._state.update(...) } |
Best Practices
✅ Do This:
// Use the linkedSignal for the reset-on-change
readonly selected = linkedSignal(() => this.options()[0]); // ✅
// Use the resource for the async data
readonly users = resource({ request, loader }); // ✅
// Use the rxResource for the HttpClient
readonly users = rxResource({ request, loader }); // ✅
// Use the asReadonly for the public view
readonly items = this._items.asReadonly(); // ✅
// Use the computed for the derived
readonly total = computed(() => this.subtotal() + this.tax()); // ✅
// Use the untracked for the non-dependency read
const previous = untracked(() => this.previous()); // ✅
// Use the effect for the persistence
effect(() => saveSettings(this._settings())); // ✅
// Use the methods for the writes
add(item: Item) { this._items.update((list) => [...list, item]); } // ✅
❌ Don’t Do This:
// Don't use the effect to write the derived value
effect(() => this.doubled.set(this.count() * 2)); // ⚠️
// Don't use the switchMap + toSignal for the async data
toSignal(toObservable(this.filter).pipe(switchMap(fetch))); // ⚠️
// Don't expose the writable signal
readonly items = this._items; // ⚠️
// Don't use the plain signal for the reset-on-change
readonly selected = signal(''); // ⚠️
// Don't overuse the untracked
untracked(() => this.a()); // the default is the tracking // ⚠️
// Don't use the effect for the data fetch
effect(() => { this.http.get('/api').subscribe(); }); // ⚠️
// Don't forget the linkedSignal's source
// The value resets when the source changes. // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| The effect writes the derived | The wrong tool | Use linkedSignal |
The switchMap + the toSignal | The verbose | Use resource |
| The writable exposed | The external writes | Use asReadonly |
| The plain signal for the reset | The manual reset | Use linkedSignal |
The untracked overuse | The lost tracking | Use it deliberately |
| The effect fetches | The wrong tool | Use resource |
| The missing reset | The stale value | The linkedSignal |
Real-World Examples
1. The linkedSignal
readonly selected = linkedSignal(() => this.options()[0]);
2. The resource
readonly users = resource({ request, loader });
3. The rxResource
readonly users = rxResource({ request, loader });
4. The signal store
private readonly items = signal<Item[]>([]);
readonly cartItems = this.items.asReadonly();
5. The computed chain
const total = computed(() => subtotal() + tax() + shipping());
6. The untracked
const bValue = untracked(() => b());
7. The untracked in the effect
const prev = untracked(() => this.previous());
8. The persistence
effect(() => saveSettings(this._settings()));
9. The asReadonly
readonly items = this._items.asReadonly();
10. The write method
add(item: Item) { this._items.update((list) => [...list, item]); }
Visual: The linkedSignal
┌──────────────────────────────────────────────────────────┐
│ readonly options = input.required<string[]>(); │
│ readonly selected = linkedSignal(() => this.options()[0] ?? '');│
│ │
│ INITIAL │
│ options = ['a', 'b', 'c'] │
│ selected = 'a' (derived) │
│ │
│ USER SELECTS 'b' │
│ selected.set('b') │
│ selected = 'b' (overrides) │
│ │
│ OPTIONS CHANGE │
│ options = ['x', 'y', 'z'] │
│ selected = 'x' (resets) │
│ │
│ The selected resets when the source changes, │
│ but the user can override it in between. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The resource
┌──────────────────────────────────────────────────────────┐
│ readonly filter = signal('') │
│ │
│ readonly users = resource({ │
│ request: () => ({ filter: this.filter() }), │
│ loader: ({ request, abortSignal }) => fetch(...), │
│ }); │
│ │
│ users.status() → 'idle' | 'loading' | 'error' | │
│ 'success' | 'reloading' │
│ users.value() → User[] | undefined │
│ users.error() → unknown │
│ users.reload() → re-run │
│ │
│ filter.set('alice') │
│ │ │
│ ▼ │
│ The request re-runs. │
│ The previous request is aborted. │
│ status = 'loading' │
│ │ │
│ ▼ │
│ The loader completes. │
│ value = the result │
│ status = 'success' │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The Signal Store
┌──────────────────────────────────────────────────────────┐
│ @Injectable({ providedIn: 'root' }) │
│ export class CartStore { │
│ private readonly items = signal<CartItem[]>([]); │
│ readonly cartItems = this.items.asReadonly(); │
│ readonly subtotal = computed(() => ...); │
│ readonly total = computed(() => ...); │
│ │
│ add(item: CartItem): void { │
│ this.items.update((list) => [...list, item]); │
│ } │
│ } │
│ │
│ The private writable. │
│ The public read-only. │
│ The computed derived. │
│ The methods the writes. │
│ │
│ The store is the single writer. │
│ The components are the readers. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The untracked
┌──────────────────────────────────────────────────────────┐
│ const sum = computed(() => { │
│ const aValue = a(); // tracked │
│ const bValue = untracked(() => b()); // not tracked │
│ return aValue + bValue; │
│ }); │
│ │
│ a.set(10) → the sum recomputes │
│ b.set(20) → the sum does not recompute │
│ │
│ The sum depends on the a but not the b. │
│ │
│ The untracked is the escape hatch from the tracking. │
│ The use should be the deliberate. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The Persistence
┌──────────────────────────────────────────────────────────┐
│ @Injectable({ providedIn: 'root' }) │
│ export class SettingsStore { │
│ private readonly _settings = signal(loadSettings()); │
│ readonly settings = this._settings.asReadonly(); │
│ │
│ constructor() { │
│ effect(() => saveSettings(this._settings())); │
│ } │
│ │
│ update(partial: Partial<Settings>): void { │
│ this._settings.update((s) => ({ ...s, ...partial }));│
│ } │
│ } │
│ │
│ The initial value: loadSettings(). │
│ The save: the effect. │
│ The update: the method. │
│ The read: the asReadonly. │
│ │
└──────────────────────────────────────────────────────────┘
Summary
| API | Purpose |
|---|---|
linkedSignal | The reset-on-change state |
resource | The Promise-based async data |
rxResource | The Observable-based async data |
untracked | The read without the tracking |
| The signal store | The shared state |
The computed chain | The derived values |
| The persistence | The effect + the localStorage |
Key takeaways:
- The
linkedSignalis the reset-on-change state — the value derives from a source and resets when the source changes, but the component can write it in between - The
linkedSignal‘s computation can be custom — thecomputationreceives the source and the previous value, and it decides whether to reset - The
resourcewraps the async operation — therequestproduces the parameters, theloaderperforms the operation, and thevalue,status, anderrorare the signals - The
rxResourceis the Observable-based variant — it integrates with theHttpClient, and the two are the choice - The signal-based store holds the state in the private signals and exposes the read-only views — the methods are the writes, and the store is the single writer
- The
computedchain is the composition — the complex derivation is built from the small pieces, and the chain is the graph - The
untrackedreads a signal without the tracking — it is the escape hatch from the tracking, and the use should be the deliberate - The persistence is the effect that saves the state — the initialization reads the storage, and the effect saves the changes
- The signal-based store is the modern replacement for the
BehaviorSubject-based store — the signals are synchronous, and thecomputedis the derivation - The patterns compose — the
linkedSignalderives from theresource‘s value, thecomputedcombines the signals, and the store uses thelinkedSignalfor the local state
Remember: The advanced signal patterns are the tools for the state that has more than one input. The linkedSignal handles the reset-on-change, the resource handles the async data, the signal store handles the shared state, the computed chain handles the derived values, and the untracked handles the reads that should not track. The patterns compose, and the composition is the model. The signals are the state, and the patterns are the way to structure the complex state.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!