Angular Signals
Read one Angular interview question at a time, then flip for the complete explanation, example, and code.
Read one Angular interview question at a time, then flip for the complete explanation, example, and code.
Angular Interview Questions · Angular Signals Companion Article
Angular Signals provide a reactive way to store, read, derive, and respond to application state. Instead of repeatedly checking every value to determine whether something changed, Angular can track which parts of the application depend on a signal and react when that signal changes.
Signals are especially useful for component state, derived UI values, shared service state, filters, counters, selections, form-related state, configuration values, and other data that changes while an application is running.
A signal can contain almost any type of value:
nullundefinedThe important difference between a normal variable and a signal is that Angular can track signal reads inside reactive contexts.
A normal TypeScript variable stores a value.
count = 0;
Changing the variable changes its value:
this.count = 5;
However, a signal is a reactive value container.
import { signal } from '@angular/core';
count = signal(0);
The value is read by calling the signal:
console.log(this.count());
The parentheses are important.
A signal behaves like a getter function when its value is read.
| Normal Variable | Signal |
|---|---|
Read with count | Read with count() |
| Update using assignment | Update using set() or update() |
| Does not create a signal dependency | Can participate in Angular's reactive dependency graph |
| Good for non-reactive values | Good for reactive application state |
| Cannot create computed dependencies automatically | Works naturally with computed() and effect() |
Signals do not mean that every variable in an Angular application should become a signal. Constants, temporary method variables, and values that never participate in reactive UI state can remain normal TypeScript values.
Angular provides the signal() function for creating writable signals.
import { signal } from '@angular/core';
count = signal(0);
Here:
signal() creates the signal.0 is its initial value.number.You can explicitly specify the type when necessary.
name = signal<string>('Dattatray');
Boolean signal:
isLoggedIn = signal(false);
Array signal:
skills = signal<string[]>(['Angular', 'Java', 'TypeScript']);
Object signal:
user = signal({
id: 1,
name: 'Rahul'
});
Signals can therefore represent both small UI values and more structured application state.
A signal's value is read by calling it as a function.
count = signal(10);
showCount(): void {
console.log(this.count());
}
Output:
10
Do not write:
console.log(this.count);
That refers to the signal itself rather than its current value.
Signals can be read directly from Angular templates.
@Component({
template: `
<h2>Count: {{ count() }}</h2>
`
})
export class CounterComponent {
count = signal(0);
}
When the signal changes, Angular knows that the template depends on the signal and can update the relevant view.
Signals used in an OnPush component template are tracked by Angular, and a signal change marks that component so its view can be updated during change detection.
Writable signals support two primary update operations:
set()update()They solve slightly different problems.
set()Use set() when the complete new value is already known.
count = signal(0);
reset(): void {
this.count.set(0);
}
Another example:
status = signal('offline');
goOnline(): void {
this.status.set('online');
}
For a boolean:
isVisible = signal(false);
show(): void {
this.isVisible.set(true);
}
Think of set() as:
Replace the current value with this new value.
update()Use update() when the new value depends on the current value.
count = signal(0);
increment(): void {
this.count.update(value => value + 1);
}
Angular passes the current signal value to the callback.
If the signal currently contains:
5
then:
this.count.update(value => value + 1);
produces:
6
decrement(): void {
this.count.update(value => value - 1);
}
isOpen = signal(false);
toggle(): void {
this.isOpen.update(value => !value);
}
Prefer producing a new array:
items = signal<string[]>([]);
addItem(item: string): void {
this.items.update(items => [...items, item]);
}
user = signal({
name: 'Amit',
age: 25
});
updateAge(): void {
this.user.update(user => ({
...user,
age: 26
}));
}
Creating new object or array references is generally easier to reason about with signal change detection than mutating existing objects in place.
set() vs update()| Requirement | Use |
|---|---|
| Replace with a known value | set() |
| Reset state | set() |
| Increment/decrement | update() |
| Toggle a boolean | update() |
| New value depends on old value | update() |
| Replace an entire object | Usually set() |
| Modify object state immutably | Usually update() |
Example:
count.set(100);
means:
The new value must be 100.
While:
count.update(value => value + 1);
means:
Calculate the new value using the existing value.
A signal created with signal() is normally a writable signal.
Its TypeScript type is WritableSignal<T>.
import { signal, WritableSignal } from '@angular/core';
count: WritableSignal<number> = signal(0);
A writable signal can:
set()update()Example:
count = signal(0);
increment(): void {
this.count.update(count => count + 1);
}
reset(): void {
this.count.set(0);
}
Writable signals are appropriate when the owner of the state should have permission to modify it directly.
Sometimes other parts of an application should be able to read state but should not be allowed to modify it.
A writable signal can be exposed through asReadonly().
private countState = signal(0);
count = this.countState.asReadonly();
External code can read:
this.count();
but cannot do:
this.count.set(10);
because the exposed signal is read-only.
A common service pattern is:
@Injectable({
providedIn: 'root'
})
export class CounterService {
private readonly _count = signal(0);
readonly count = this._count.asReadonly();
increment(): void {
this._count.update(value => value + 1);
}
reset(): void {
this._count.set(0);
}
}
This gives the service control over how its state changes.
One important detail is that asReadonly() prevents consumers from calling writable signal APIs, but it does not automatically make nested objects or arrays deeply immutable.
Many application values are derived from other values.
Consider:
price = signal(500);
quantity = signal(3);
The total should always be:
price × quantity
Instead of manually updating another signal every time either value changes, Angular provides computed signals.
import { computed, signal } from '@angular/core';
price = signal(500);
quantity = signal(3);
total = computed(() => this.price() * this.quantity());
Read it normally:
console.log(this.total());
If:
price = 500
quantity = 3
then:
total = 1500
Change the quantity:
this.quantity.set(4);
The next read of total() produces:
2000
No manual synchronization is necessary.
computed()computed() creates a read-only signal whose value is derived from other reactive values.
firstName = signal('Rahul');
lastName = signal('Patil');
fullName = computed(
() => `${this.firstName()} ${this.lastName()}`
);
Template:
<p>{{ fullName() }}</p>
If either source signal changes, the computed value becomes invalid and is recalculated when required.
Computed signals are lazy and memoized: the calculation is performed when the computed value is read, and Angular can reuse the cached result until one of its tracked dependencies changes.
Filtering:
products = signal([
{ name: 'Laptop', active: true },
{ name: 'Mouse', active: false },
{ name: 'Keyboard', active: true }
]);
activeProducts = computed(() =>
this.products().filter(product => product.active)
);
Number of products:
productCount = computed(() => this.products().length);
UI state:
isEmpty = computed(() => this.products().length === 0);
Multiple conditions:
canCheckout = computed(
() => this.cartItems().length > 0 && this.isLoggedIn()
);
This is invalid:
total.set(1000);
A computed signal represents derived state and is read-only.
If a value needs both automatic derivation and manual modification, linkedSignal() may be more appropriate.
Angular automatically discovers signal dependencies while reactive code runs.
Consider:
firstName = signal('Amit');
lastName = signal('Patil');
fullName = computed(() =>
`${this.firstName()} ${this.lastName()}`
);
Angular can determine that fullName depends on:
firstName
lastName
You do not manually register these dependencies.
Conceptually:
firstName ──┐
├──> fullName
lastName ───┘
When either dependency changes, Angular knows that the cached computed value is no longer current.
Signal dependencies can change depending on which signals are actually read during a computation.
showPrice = signal(false);
price = signal(500);
label = computed(() => {
if (this.showPrice()) {
return `Price: ${this.price()}`;
}
return 'Price hidden';
});
When:
showPrice() === false
the price signal is not read.
Therefore, price does not need to be a dependency for that particular execution.
If showPrice later becomes true, the computation reads price(), so price becomes a dependency.
Dependencies can therefore be both added and removed dynamically according to the signals actually read during the latest computation.
This behavior helps Angular maintain a precise reactive dependency graph.
Derived state is not the only reason to react to changes.
Sometimes a state change needs to trigger an external action, such as:
Angular provides effect() for these cases.
import { effect, signal } from '@angular/core';
count = signal(0);
constructor() {
effect(() => {
console.log(`Count changed: ${this.count()}`);
});
}
The effect reads:
this.count()
so count becomes one of its dependencies.
When that signal changes, Angular schedules the effect to execute again.
effect()A useful example is saving a user preference.
theme = signal<'light' | 'dark'>('light');
constructor() {
effect(() => {
localStorage.setItem('theme', this.theme());
});
}
Whenever theme changes, the browser storage can be synchronized.
Another example:
currentUser = signal<string | null>(null);
constructor() {
effect(() => {
console.log('Current user:', this.currentUser());
});
}
Current Angular guidance recommends using effects primarily when signal state must interact with an imperative or non-signal API. Derived values should normally use computed(), while writable derived state is often better represented by linkedSignal().
A common mistake is:
price = signal(100);
quantity = signal(2);
total = signal(0);
constructor() {
effect(() => {
this.total.set(this.price() * this.quantity());
});
}
This works conceptually, but the state model is unnecessarily complicated.
total is derived state, so use:
total = computed(
() => this.price() * this.quantity()
);
The data model becomes clearer:
Source state
↓
Computed state
↓
UI
Rather than:
Source state
↓
Effect
↓
Another writable state
↓
UI
This distinction becomes increasingly important as an application's state grows.
Angular needs to determine whether a signal's new value is actually different from its previous value.
By default, signals use Object.is() equality.
For primitive values:
count = signal(10);
this.count.set(10);
the value is equal to the previous value.
Objects are different because JavaScript normally compares their references.
user = signal({
id: 1,
name: 'Amit'
});
This new object:
this.user.set({
id: 1,
name: 'Amit'
});
has a different reference even though its properties contain the same information.
A custom equality function can be supplied when creating certain signals.
Simple example:
temperature = signal(20, {
equal: (previous, current) => previous === current
});
For objects, an application could define domain-specific equality logic:
user = signal(
{ id: 1, name: 'Amit' },
{
equal: (a, b) =>
a.id === b.id &&
a.name === b.name
}
);
If the equality function considers the old and new values equal, dependent consumers do not need to react as though meaningful state changed.
Custom equality should be used intentionally. Deep comparison of large structures can itself have a performance cost.
Angular supports equality functions on writable signals and computed signals.
Consider:
user = signal({
name: 'Amit',
age: 25
});
Avoid relying on code such as:
this.user().age = 26;
The object stored by the signal has been mutated, but the writable signal API was not used to establish a new signal value.
Prefer:
this.user.update(user => ({
...user,
age: 26
}));
Similarly, instead of:
this.items().push('Angular');
prefer:
this.items.update(items => [
...items,
'Angular'
]);
This immutable update style makes state transitions explicit and predictable.
untracked()Usually, when a signal is read inside a reactive context such as an effect, Angular tracks that signal as a dependency.
Sometimes a value needs to be read without becoming a dependency.
Consider:
currentUser = signal('Amit');
counter = signal(0);
Suppose an effect should execute when the user changes, but the log should also show the current counter value.
Without untracked():
effect(() => {
console.log(
this.currentUser(),
this.counter()
);
});
Both signals become dependencies.
Changing counter would therefore also cause the effect to run.
Use:
import { effect, untracked } from '@angular/core';
effect(() => {
console.log(
this.currentUser(),
untracked(this.counter)
);
});
Now:
currentUser → tracked dependency
counter → incidental read
untracked() can therefore distinguish between:
A signal that should trigger the reactive operation
and:
A signal whose current value merely needs to be inspected
Angular also allows untracked() to wrap code that might internally read signals so those reads do not accidentally become dependencies of the surrounding reactive operation.
Use it sparingly; automatic dependency tracking should remain the normal approach.
Effects may create resources that should not continue forever.
Examples include:
An effect can register cleanup logic using onCleanup.
effect((onCleanup) => {
const user = this.currentUser();
const timer = setTimeout(() => {
console.log(`Current user: ${user}`);
}, 1000);
onCleanup(() => {
clearTimeout(timer);
});
});
The cleanup callback runs before the next effect execution and when the effect is destroyed, allowing work created by the previous execution to be cancelled. Angular also automatically destroys effects associated with destroyed components or directives.
effect((onCleanup) => {
const intervalId = setInterval(() => {
console.log(this.status());
}, 5000);
onCleanup(() => {
clearInterval(intervalId);
});
});
Cleanup prevents abandoned resources from continuing to run after they are no longer needed.
linkedSignal()Sometimes a value should normally be derived from another value but still remain writable.
This requirement falls between a normal writable signal and a computed signal.
Consider a list of available delivery methods:
shippingOptions = signal([
'Standard',
'Express',
'Pickup'
]);
The selected option should initially be the first option.
A normal signal could be:
selectedOption = signal(
this.shippingOptions()[0]
);
But if shippingOptions changes completely, the selection does not automatically reset.
linkedSignal() solves this type of dependent writable state.
import { linkedSignal, signal } from '@angular/core';
shippingOptions = signal([
'Standard',
'Express',
'Pickup'
]);
selectedOption = linkedSignal(
() => this.shippingOptions()[0]
);
The value follows its reactive computation when its source dependencies change.
However, unlike computed(), the result is writable.
this.selectedOption.set('Express');
linkedSignal() is therefore useful for state that has:
Reactive default + manual override
Angular documents linkedSignal() as a writable signal initialized and reset through a linked reactive computation.
computed() vs linkedSignal()This difference is important.
discount = computed(
() => this.customerType() === 'premium' ? 20 : 0
);
The value should always follow the calculation.
The application should not manually do:
discount.set(50);
discount = linkedSignal(
() => this.customerType() === 'premium' ? 20 : 0
);
Now the value normally follows the computation but can also be changed:
this.discount.set(30);
| Feature | signal() | computed() | linkedSignal() |
|---|---|---|---|
| Writable | Yes | No | Yes |
| Can derive from signals | Manually | Yes | Yes |
| Tracks dependencies | No derivation | Yes | Yes |
| Manual override | Yes | No | Yes |
| Best for source state | Yes | No | Sometimes |
| Best for pure derived state | No | Yes | No |
| Best for writable dependent state | No | No | Yes |
Angular's current tutorial describes this distinction as computed signals being read-only derived state, while linked signals remain writable while maintaining their reactive connection.
linkedSignal() with Previous StateMore advanced cases may need the previous linked value.
Imagine:
shippingOptions
changes but you want to keep the user's previous selection if that option still exists.
Conceptually:
New options arrive
↓
Is previous selection still valid?
↓
Yes → keep it
No → choose a default
A source/computation form of linkedSignal() supports previous state information.
interface ShippingMethod {
id: number;
name: string;
}
Example:
shippingOptions = signal<ShippingMethod[]>([
{ id: 1, name: 'Standard' },
{ id: 2, name: 'Express' }
]);
selectedOption = linkedSignal({
source: this.shippingOptions,
computation: (options, previous) => {
const previousSelection = previous?.value;
return options.find(
option => option.id === previousSelection?.id
) ?? options[0];
}
});
This pattern is useful for selections, filters, tabs, preferred options, and other UI state that should remain valid when its source data changes.
Signals work naturally with Angular component state.
Example:
import {
ChangeDetectionStrategy,
Component,
computed,
signal
} from '@angular/core';
@Component({
selector: 'app-cart',
template: `
<h2>Shopping Cart</h2>
<p>Quantity: {{ quantity() }}</p>
<p>Price: ₹{{ price() }}</p>
<p>Total: ₹{{ total() }}</p>
<button (click)="increase()">
Add
</button>
<button (click)="decrease()">
Remove
</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CartComponent {
price = signal(500);
quantity = signal(1);
total = computed(
() => this.price() * this.quantity()
);
increase(): void {
this.quantity.update(value => value + 1);
}
decrease(): void {
this.quantity.update(
value => Math.max(1, value - 1)
);
}
}
The component contains:
price → source signal
quantity → source signal
total → computed signal
The template simply consumes the current reactive values.
Consider a product search page.
products = signal([
'Angular Course',
'Java Course',
'Python Course',
'Spring Boot Course'
]);
searchText = signal('');
Create filtered results:
filteredProducts = computed(() => {
const search = this.searchText()
.trim()
.toLowerCase();
if (!search) {
return this.products();
}
return this.products().filter(product =>
product.toLowerCase().includes(search)
);
});
Update the search signal:
updateSearch(event: Event): void {
const input = event.target as HTMLInputElement;
this.searchText.set(input.value);
}
Template:
<input
type="text"
[value]="searchText()"
(input)="updateSearch($event)"
placeholder="Search courses"
/>
<ul>
@for (product of filteredProducts(); track product) {
<li>{{ product }}</li>
} @empty {
<li>No products found.</li>
}
</ul>
The state flow becomes:
User types
↓
searchText changes
↓
filteredProducts becomes stale
↓
Template reads filteredProducts
↓
Updated list is displayed
No separate method is required to manually synchronize the filtered list.
Signals become particularly useful when multiple components need access to the same state.
A service can own the writable state and expose read-only signals to components.
import {
computed,
Injectable,
signal
} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CartService {
private readonly _items = signal<string[]>([]);
readonly items = this._items.asReadonly();
readonly itemCount = computed(
() => this._items().length
);
addItem(item: string): void {
this._items.update(items => [
...items,
item
]);
}
removeItem(item: string): void {
this._items.update(items =>
items.filter(current => current !== item)
);
}
clear(): void {
this._items.set([]);
}
}
Component:
@Component({
selector: 'app-cart-summary',
template: `
<p>Items: {{ cart.itemCount() }}</p>
@for (item of cart.items(); track item) {
<p>{{ item }}</p>
}
`
})
export class CartSummaryComponent {
readonly cart = inject(CartService);
}
This pattern provides clear ownership:
CartService
│
├── private writable state
│
├── public read-only signals
│
├── computed derived state
│
└── public state-changing methods
↓
Components consume state
This prevents unrelated components from modifying shared state arbitrarily.
A useful design rule is to place state close to where it belongs.
Use a component signal for state such as:
Example:
isMenuOpen = signal(false);
Use service-owned signals when state must be shared across multiple parts of the application.
Examples:
Do not automatically move every signal into a global service. Excessive global state makes applications harder to understand and maintain.
A clean signal-based state model normally separates three responsibilities.
Information that can genuinely change independently.
price = signal(500);
quantity = signal(2);
discountPercent = signal(10);
Information calculated from source state.
subtotal = computed(
() => this.price() * this.quantity()
);
discount = computed(
() => this.subtotal() * this.discountPercent() / 100
);
finalPrice = computed(
() => this.subtotal() - this.discount()
);
Actions performed outside the reactive state model.
effect(() => {
localStorage.setItem(
'quantity',
String(this.quantity())
);
});
A useful mental model is:
signal()
↓
Source State
↓
computed() / linkedSignal()
↓
Derived UI State
↓
Template
effect()
↓
External / imperative system
Keeping these responsibilities separate makes signal-based applications easier to maintain.
Arrays are common signal values.
tasks = signal<string[]>([]);
addTask(task: string): void {
this.tasks.update(tasks => [
...tasks,
task
]);
}
removeTask(task: string): void {
this.tasks.update(tasks =>
tasks.filter(item => item !== task)
);
}
this.tasks.set([
'Learn Signals',
'Learn Routing',
'Learn Forms'
]);
completedTasks = computed(() =>
this.tasks().filter(task => task.completed)
);
Signals work especially well when a single source array feeds multiple computed views.
Objects can also be stored inside signals.
profile = signal({
name: 'Rahul',
city: 'Pune',
active: true
});
Update one property:
changeCity(city: string): void {
this.profile.update(profile => ({
...profile,
city
}));
}
The spread operator preserves existing properties while returning a new object.
()Incorrect:
<p>{{ count }}</p>
Correct:
<p>{{ count() }}</p>
Incorrect:
this.count = 10;
when count is a signal.
Correct:
this.count.set(10);
set() for Calculated UpdatesLess appropriate:
this.count.set(this.count() + 1);
Prefer:
this.count.update(value => value + 1);
Avoid:
total = signal(0);
when total always equals:
price × quantity
Prefer:
total = computed(
() => this.price() * this.quantity()
);
effect() for Derived StateAvoid using an effect just to keep one signal synchronized with another.
Prefer:
computed()
or, when the derived value also needs manual changes:
linkedSignal()
Avoid:
this.items().push(newItem);
Prefer:
this.items.update(items => [
...items,
newItem
]);
Avoid:
this.user().name = 'Rahul';
Prefer:
this.user.update(user => ({
...user,
name: 'Rahul'
}));
Instead of:
items = signal<string[]>([]);
in a shared service, consider:
private readonly _items = signal<string[]>([]);
readonly items = this._items.asReadonly();
Then expose meaningful operations such as:
addItem()
removeItem()
clear()
This protects state ownership.
untracked()Dependency tracking normally works automatically.
Do not use untracked() just because a reactive operation has several dependencies.
Use it only when a signal read is intentionally incidental and should not trigger reevaluation.
Effects that create timers or external resources should clean them up when necessary.
effect((onCleanup) => {
const id = setInterval(() => {
console.log('Running');
}, 1000);
onCleanup(() => clearInterval(id));
});
A simple decision process is:
Do I need to store independently changing state?
|
Yes
|
signal()
Is this value completely derived from other state?
|
Yes
|
computed()
Is it derived but must also be manually writable?
|
Yes
|
linkedSignal()
Do I need to synchronize reactive state with an
external or imperative API?
|
Yes
|
effect()
Do I need to read a signal without creating
a reactive dependency?
|
Yes
|
untracked()
This approach keeps state responsibilities explicit.
A small shopping-cart state model demonstrates how the APIs work together.
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
Service:
@Injectable({
providedIn: 'root'
})
export class CartService {
private readonly _items = signal<CartItem[]>([]);
readonly items = this._items.asReadonly();
readonly itemCount = computed(() =>
this._items().reduce(
(count, item) => count + item.quantity,
0
)
);
readonly subtotal = computed(() =>
this._items().reduce(
(total, item) =>
total + item.price * item.quantity,
0
)
);
readonly isEmpty = computed(
() => this._items().length === 0
);
addItem(item: CartItem): void {
this._items.update(items => [
...items,
item
]);
}
removeItem(id: number): void {
this._items.update(items =>
items.filter(item => item.id !== id)
);
}
clearCart(): void {
this._items.set([]);
}
}
The design contains one source of truth:
_items
Everything else is derived:
_items
├──> items
├──> itemCount
├──> subtotal
└──> isEmpty
This is generally easier to maintain than storing and manually synchronizing separate values for item count, subtotal, and empty status.
Signals are particularly useful for:
They provide a straightforward way to represent synchronous reactive state.
Basic signal APIs such as signal() and computed() are synchronous. Angular also provides APIs designed to integrate asynchronous work with signal-based applications, including Resources.
This means signals should not be treated as a rule saying:
Replace every Observable with a signal.
Angular applications can use signals and RxJS together.
A practical architecture may use:
HTTP / event streams / complex async workflows
↓
RxJS
↓
Application State
↓
Signals
↓
Component Template
The correct abstraction depends on whether the problem is primarily synchronous state, asynchronous event streams, or a combination of both.
Signals encourage a state model based on explicit dependencies.
Instead of manually coordinating several properties:
quantity changes
↓
manually calculate subtotal
↓
manually calculate tax
↓
manually calculate total
↓
update UI
the dependencies can be modeled directly:
price ────────┐
├──> subtotal ───┐
quantity ─────┘ │
├──> finalTotal
taxRate ────────────────────────┘
In Angular:
price = signal(500);
quantity = signal(2);
taxRate = signal(0.18);
subtotal = computed(
() => this.price() * this.quantity()
);
tax = computed(
() => this.subtotal() * this.taxRate()
);
finalTotal = computed(
() => this.subtotal() + this.tax()
);
Each computed value declares how it depends on the source state.
Angular tracks the dependency relationships automatically.
computed() instead of duplicating derived state.linkedSignal() only when derived state genuinely needs a writable override.effect() for appropriate side effects rather than routine state propagation.selectedUser, isLoading, and filteredProducts.untracked() only when deliberately excluding a read from dependency tracking.| API | Purpose | Writable |
|---|---|---|
signal() | Create reactive source state | Yes |
set() | Replace a writable signal value | Yes |
update() | Calculate a new value from the current value | Yes |
asReadonly() | Expose writable state as read-only | No |
computed() | Create derived state | No |
effect() | React to changes with side effects | N/A |
untracked() | Read without creating a dependency | N/A |
linkedSignal() | Create writable dependent state | Yes |
The easiest way to understand Angular Signals is to separate state, derivation, and effects.
signal()
|
| stores source state
↓
computed()
|
| derives read-only state
↓
Template
When derived state must also be writable:
Source Signal
↓
linkedSignal()
↓
Writable dependent state
When reactive state needs to interact with something outside the signal graph:
Signal
↓
effect()
↓
localStorage / logging / third-party API / external behavior
For controlled state exposure:
Private WritableSignal
↓
asReadonly()
↓
Public Read-Only Signal
↓
Components
Understanding these relationships is more important than memorizing individual methods. Once the source of truth and dependency direction are clear, Angular's signal APIs provide a concise way to build predictable reactive component and service state.