Angular Signals

Read one Angular interview question at a time, then flip for the complete explanation, example, and code.

0/60 Known filtered set
Level
Read Full Guide Question: 1 of 60

Angular Interview Questions · Angular Signals Companion Article

Chapter 12: Angular Signals

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:

  • Number
  • String
  • Boolean
  • Object
  • Array
  • Custom TypeScript type
  • null
  • undefined

The important difference between a normal variable and a signal is that Angular can track signal reads inside reactive contexts.

Signal Fundamentals

A normal TypeScript variable stores a value.

TypeScript
count = 0;

Changing the variable changes its value:

TypeScript
this.count = 5;

However, a signal is a reactive value container.

TypeScript
import { signal } from '@angular/core';

count = signal(0);

The value is read by calling the signal:

TypeScript
console.log(this.count());

The parentheses are important.

A signal behaves like a getter function when its value is read.

Normal Variable vs Signal

Normal VariableSignal
Read with countRead with count()
Update using assignmentUpdate using set() or update()
Does not create a signal dependencyCan participate in Angular's reactive dependency graph
Good for non-reactive valuesGood for reactive application state
Cannot create computed dependencies automaticallyWorks 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.

Creating Signals

Angular provides the signal() function for creating writable signals.

TypeScript
import { signal } from '@angular/core';

count = signal(0);

Here:

  • signal() creates the signal.
  • 0 is its initial value.
  • Angular infers the value type as number.

You can explicitly specify the type when necessary.

TypeScript
name = signal<string>('Dattatray');

Boolean signal:

TypeScript
isLoggedIn = signal(false);

Array signal:

TypeScript
skills = signal<string[]>(['Angular', 'Java', 'TypeScript']);

Object signal:

TypeScript
user = signal({
  id: 1,
  name: 'Rahul'
});

Signals can therefore represent both small UI values and more structured application state.

Reading Signals

A signal's value is read by calling it as a function.

TypeScript
count = signal(10);

showCount(): void {
  console.log(this.count());
}

Output:

Text
10

Do not write:

TypeScript
console.log(this.count);

That refers to the signal itself rather than its current value.

Reading a Signal in a Template

Signals can be read directly from Angular templates.

TypeScript
@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.

Updating Signals

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.

TypeScript
count = signal(0);

reset(): void {
  this.count.set(0);
}

Another example:

TypeScript
status = signal('offline');

goOnline(): void {
  this.status.set('online');
}

For a boolean:

TypeScript
isVisible = signal(false);

show(): void {
  this.isVisible.set(true);
}

Think of set() as:

Text
Replace the current value with this new value.

update()

Use update() when the new value depends on the current value.

TypeScript
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:

Text
5

then:

TypeScript
this.count.update(value => value + 1);

produces:

Text
6

Decrement Example

TypeScript
decrement(): void {
  this.count.update(value => value - 1);
}

Toggle Example

TypeScript
isOpen = signal(false);

toggle(): void {
  this.isOpen.update(value => !value);
}

Updating an Array

Prefer producing a new array:

TypeScript
items = signal<string[]>([]);

addItem(item: string): void {
  this.items.update(items => [...items, item]);
}

Updating an Object

TypeScript
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()

RequirementUse
Replace with a known valueset()
Reset stateset()
Increment/decrementupdate()
Toggle a booleanupdate()
New value depends on old valueupdate()
Replace an entire objectUsually set()
Modify object state immutablyUsually update()

Example:

TypeScript
count.set(100);

means:

Text
The new value must be 100.

While:

TypeScript
count.update(value => value + 1);

means:

Text
Calculate the new value using the existing value.

Writable Signals

A signal created with signal() is normally a writable signal.

Its TypeScript type is WritableSignal<T>.

TypeScript
import { signal, WritableSignal } from '@angular/core';

count: WritableSignal<number> = signal(0);

A writable signal can:

  • Be read
  • Be changed using set()
  • Be changed using update()
  • Be exposed as read-only

Example:

TypeScript
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.

Read-Only Signals

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().

TypeScript
private countState = signal(0);

count = this.countState.asReadonly();

External code can read:

TypeScript
this.count();

but cannot do:

TypeScript
this.count.set(10);

because the exposed signal is read-only.

A common service pattern is:

TypeScript
@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.

Computed Signals

Many application values are derived from other values.

Consider:

TypeScript
price = signal(500);
quantity = signal(3);

The total should always be:

Text
price × quantity

Instead of manually updating another signal every time either value changes, Angular provides computed signals.

TypeScript
import { computed, signal } from '@angular/core';

price = signal(500);
quantity = signal(3);

total = computed(() => this.price() * this.quantity());

Read it normally:

TypeScript
console.log(this.total());

If:

Text
price = 500
quantity = 3

then:

Text
total = 1500

Change the quantity:

TypeScript
this.quantity.set(4);

The next read of total() produces:

Text
2000

No manual synchronization is necessary.

computed()

computed() creates a read-only signal whose value is derived from other reactive values.

TypeScript
firstName = signal('Rahul');
lastName = signal('Patil');

fullName = computed(
  () => `${this.firstName()} ${this.lastName()}`
);

Template:

HTML
<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.

Useful Computed Signal Examples

Filtering:

TypeScript
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:

TypeScript
productCount = computed(() => this.products().length);

UI state:

TypeScript
isEmpty = computed(() => this.products().length === 0);

Multiple conditions:

TypeScript
canCheckout = computed(
  () => this.cartItems().length > 0 && this.isLoggedIn()
);

Do Not Write to a Computed Signal

This is invalid:

TypeScript
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.

Signal Dependencies

Angular automatically discovers signal dependencies while reactive code runs.

Consider:

TypeScript
firstName = signal('Amit');
lastName = signal('Patil');

fullName = computed(() =>
  `${this.firstName()} ${this.lastName()}`
);

Angular can determine that fullName depends on:

Text
firstName
lastName

You do not manually register these dependencies.

Conceptually:

Text
firstName ──┐
            ├──> fullName
lastName ───┘

When either dependency changes, Angular knows that the cached computed value is no longer current.

Dynamic Signal Dependencies

Signal dependencies can change depending on which signals are actually read during a computation.

TypeScript
showPrice = signal(false);
price = signal(500);

label = computed(() => {
  if (this.showPrice()) {
    return `Price: ${this.price()}`;
  }

  return 'Price hidden';
});

When:

TypeScript
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.

Effects

Derived state is not the only reason to react to changes.

Sometimes a state change needs to trigger an external action, such as:

  • Writing to browser storage
  • Sending analytics information
  • Logging
  • Updating a non-signal API
  • Starting or stopping a timer
  • Integrating with a third-party library

Angular provides effect() for these cases.

TypeScript
import { effect, signal } from '@angular/core';

count = signal(0);

constructor() {
  effect(() => {
    console.log(`Count changed: ${this.count()}`);
  });
}

The effect reads:

TypeScript
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.

TypeScript
theme = signal<'light' | 'dark'>('light');

constructor() {
  effect(() => {
    localStorage.setItem('theme', this.theme());
  });
}

Whenever theme changes, the browser storage can be synchronized.

Another example:

TypeScript
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().

Do Not Use Effects to Copy State

A common mistake is:

TypeScript
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:

TypeScript
total = computed(
  () => this.price() * this.quantity()
);

The data model becomes clearer:

Text
Source state
   ↓
Computed state
   ↓
UI

Rather than:

Text
Source state
   ↓
Effect
   ↓
Another writable state
   ↓
UI

This distinction becomes increasingly important as an application's state grows.

Signal Equality

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:

TypeScript
count = signal(10);

this.count.set(10);

the value is equal to the previous value.

Objects are different because JavaScript normally compares their references.

TypeScript
user = signal({
  id: 1,
  name: 'Amit'
});

This new object:

TypeScript
this.user.set({
  id: 1,
  name: 'Amit'
});

has a different reference even though its properties contain the same information.

Custom Signal Equality

A custom equality function can be supplied when creating certain signals.

Simple example:

TypeScript
temperature = signal(20, {
  equal: (previous, current) => previous === current
});

For objects, an application could define domain-specific equality logic:

TypeScript
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.

Avoid In-Place Mutation of Signal Objects

Consider:

TypeScript
user = signal({
  name: 'Amit',
  age: 25
});

Avoid relying on code such as:

TypeScript
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:

TypeScript
this.user.update(user => ({
  ...user,
  age: 26
}));

Similarly, instead of:

TypeScript
this.items().push('Angular');

prefer:

TypeScript
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:

TypeScript
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():

TypeScript
effect(() => {
  console.log(
    this.currentUser(),
    this.counter()
  );
});

Both signals become dependencies.

Changing counter would therefore also cause the effect to run.

Use:

TypeScript
import { effect, untracked } from '@angular/core';

effect(() => {
  console.log(
    this.currentUser(),
    untracked(this.counter)
  );
});

Now:

Text
currentUser → tracked dependency
counter → incidental read

untracked() can therefore distinguish between:

Text
A signal that should trigger the reactive operation

and:

Text
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.

Signal Cleanup

Effects may create resources that should not continue forever.

Examples include:

  • Timers
  • Intervals
  • Event listeners
  • Long-running operations
  • Third-party resources

An effect can register cleanup logic using onCleanup.

TypeScript
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.

Interval Example

TypeScript
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:

TypeScript
shippingOptions = signal([
  'Standard',
  'Express',
  'Pickup'
]);

The selected option should initially be the first option.

A normal signal could be:

TypeScript
selectedOption = signal(
  this.shippingOptions()[0]
);

But if shippingOptions changes completely, the selection does not automatically reset.

linkedSignal() solves this type of dependent writable state.

TypeScript
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.

TypeScript
this.selectedOption.set('Express');

linkedSignal() is therefore useful for state that has:

Text
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.

Computed

TypeScript
discount = computed(
  () => this.customerType() === 'premium' ? 20 : 0
);

The value should always follow the calculation.

The application should not manually do:

TypeScript
discount.set(50);

Linked Signal

TypeScript
discount = linkedSignal(
  () => this.customerType() === 'premium' ? 20 : 0
);

Now the value normally follows the computation but can also be changed:

TypeScript
this.discount.set(30);

Comparison

Featuresignal()computed()linkedSignal()
WritableYesNoYes
Can derive from signalsManuallyYesYes
Tracks dependenciesNo derivationYesYes
Manual overrideYesNoYes
Best for source stateYesNoSometimes
Best for pure derived stateNoYesNo
Best for writable dependent stateNoNoYes

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 State

More advanced cases may need the previous linked value.

Imagine:

TypeScript
shippingOptions

changes but you want to keep the user's previous selection if that option still exists.

Conceptually:

Text
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.

TypeScript
interface ShippingMethod {
  id: number;
  name: string;
}

Example:

TypeScript
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 with Components

Signals work naturally with Angular component state.

Example:

TypeScript
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:

Text
price     → source signal
quantity  → source signal
total     → computed signal

The template simply consumes the current reactive values.

A Practical Component State Example

Consider a product search page.

TypeScript
products = signal([
  'Angular Course',
  'Java Course',
  'Python Course',
  'Spring Boot Course'
]);

searchText = signal('');

Create filtered results:

TypeScript
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:

TypeScript
updateSearch(event: Event): void {
  const input = event.target as HTMLInputElement;

  this.searchText.set(input.value);
}

Template:

HTML
<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:

Text
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 with Services

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.

TypeScript
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:

TypeScript
@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:

Text
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.

Component State vs Service State

A useful design rule is to place state close to where it belongs.

Component Signal

Use a component signal for state such as:

  • Modal open/closed state
  • Selected tab
  • Search input
  • Local filter
  • Accordion state
  • Temporary UI selection
  • Local counter

Example:

TypeScript
isMenuOpen = signal(false);

Service Signal

Use service-owned signals when state must be shared across multiple parts of the application.

Examples:

  • Shopping cart
  • Logged-in user information
  • Application configuration
  • Shared filters
  • Notifications
  • Feature state
  • Dashboard preferences

Do not automatically move every signal into a global service. Excessive global state makes applications harder to understand and maintain.

Signal State Design

A clean signal-based state model normally separates three responsibilities.

1. Source State

Information that can genuinely change independently.

TypeScript
price = signal(500);
quantity = signal(2);
discountPercent = signal(10);

2. Derived State

Information calculated from source state.

TypeScript
subtotal = computed(
  () => this.price() * this.quantity()
);

discount = computed(
  () => this.subtotal() * this.discountPercent() / 100
);

finalPrice = computed(
  () => this.subtotal() - this.discount()
);

3. Side Effects

Actions performed outside the reactive state model.

TypeScript
effect(() => {
  localStorage.setItem(
    'quantity',
    String(this.quantity())
  );
});

A useful mental model is:

Text
signal()
   ↓
Source State
   ↓
computed() / linkedSignal()
   ↓
Derived UI State
   ↓
Template

effect()
   ↓
External / imperative system

Keeping these responsibilities separate makes signal-based applications easier to maintain.

Signals and Arrays

Arrays are common signal values.

TypeScript
tasks = signal<string[]>([]);

Add an Item

TypeScript
addTask(task: string): void {
  this.tasks.update(tasks => [
    ...tasks,
    task
  ]);
}

Remove an Item

TypeScript
removeTask(task: string): void {
  this.tasks.update(tasks =>
    tasks.filter(item => item !== task)
  );
}

Replace All Items

TypeScript
this.tasks.set([
  'Learn Signals',
  'Learn Routing',
  'Learn Forms'
]);

Derived Array

TypeScript
completedTasks = computed(() =>
  this.tasks().filter(task => task.completed)
);

Signals work especially well when a single source array feeds multiple computed views.

Signals and Objects

Objects can also be stored inside signals.

TypeScript
profile = signal({
  name: 'Rahul',
  city: 'Pune',
  active: true
});

Update one property:

TypeScript
changeCity(city: string): void {
  this.profile.update(profile => ({
    ...profile,
    city
  }));
}

The spread operator preserves existing properties while returning a new object.

Common Angular Signal Mistakes

Mistake 1: Forgetting ()

Incorrect:

HTML
<p>{{ count }}</p>

Correct:

HTML
<p>{{ count() }}</p>

Mistake 2: Using Assignment

Incorrect:

TypeScript
this.count = 10;

when count is a signal.

Correct:

TypeScript
this.count.set(10);

Mistake 3: Using set() for Calculated Updates

Less appropriate:

TypeScript
this.count.set(this.count() + 1);

Prefer:

TypeScript
this.count.update(value => value + 1);

Mistake 4: Making Derived State Writable

Avoid:

TypeScript
total = signal(0);

when total always equals:

Text
price × quantity

Prefer:

TypeScript
total = computed(
  () => this.price() * this.quantity()
);

Mistake 5: Using effect() for Derived State

Avoid using an effect just to keep one signal synchronized with another.

Prefer:

TypeScript
computed()

or, when the derived value also needs manual changes:

TypeScript
linkedSignal()

Mistake 6: Mutating Arrays Directly

Avoid:

TypeScript
this.items().push(newItem);

Prefer:

TypeScript
this.items.update(items => [
  ...items,
  newItem
]);

Mistake 7: Mutating Objects Directly

Avoid:

TypeScript
this.user().name = 'Rahul';

Prefer:

TypeScript
this.user.update(user => ({
  ...user,
  name: 'Rahul'
}));

Mistake 8: Exposing Every Writable Signal

Instead of:

TypeScript
items = signal<string[]>([]);

in a shared service, consider:

TypeScript
private readonly _items = signal<string[]>([]);

readonly items = this._items.asReadonly();

Then expose meaningful operations such as:

TypeScript
addItem()
removeItem()
clear()

This protects state ownership.

Mistake 9: Overusing 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.

Mistake 10: Ignoring Effect Cleanup

Effects that create timers or external resources should clean them up when necessary.

TypeScript
effect((onCleanup) => {
  const id = setInterval(() => {
    console.log('Running');
  }, 1000);

  onCleanup(() => clearInterval(id));
});

Choosing the Correct Signal API

A simple decision process is:

Text
Do I need to store independently changing state?
        |
       Yes
        |
     signal()
Text
Is this value completely derived from other state?
        |
       Yes
        |
    computed()
Text
Is it derived but must also be manually writable?
        |
       Yes
        |
 linkedSignal()
Text
Do I need to synchronize reactive state with an
external or imperative API?
        |
       Yes
        |
     effect()
Text
Do I need to read a signal without creating
a reactive dependency?
        |
       Yes
        |
    untracked()

This approach keeps state responsibilities explicit.

Practical Signal Example: Shopping Cart

A small shopping-cart state model demonstrates how the APIs work together.

TypeScript
interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

Service:

TypeScript
@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:

Text
_items

Everything else is derived:

Text
_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.

When Signals Are a Good Fit

Signals are particularly useful for:

  • Component UI state
  • Derived values
  • Search and filtering
  • Selection state
  • Dashboard state
  • Shopping-cart state
  • Shared service state
  • User preferences
  • Feature configuration
  • Reactive calculations
  • State-driven template rendering

They provide a straightforward way to represent synchronous reactive state.

Signals and Asynchronous Data

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:

Text
Replace every Observable with a signal.

Angular applications can use signals and RxJS together.

A practical architecture may use:

Text
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 in Modern Angular Architecture

Signals encourage a state model based on explicit dependencies.

Instead of manually coordinating several properties:

Text
quantity changes
   ↓
manually calculate subtotal
   ↓
manually calculate tax
   ↓
manually calculate total
   ↓
update UI

the dependencies can be modeled directly:

Text
price ────────┐
              ├──> subtotal ───┐
quantity ─────┘                 │
                                ├──> finalTotal
taxRate ────────────────────────┘

In Angular:

TypeScript
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.

Angular Signals Best Practices

  • Keep source state as small and clear as possible.
  • Use computed() instead of duplicating derived state.
  • Use linkedSignal() only when derived state genuinely needs a writable override.
  • Use effect() for appropriate side effects rather than routine state propagation.
  • Expose shared service state as read-only when consumers should not modify it directly.
  • Prefer immutable object and array updates.
  • Keep state close to the component that owns it unless it genuinely needs to be shared.
  • Use descriptive signal names such as selectedUser, isLoading, and filteredProducts.
  • Register cleanup for resources created inside effects.
  • Use untracked() only when deliberately excluding a read from dependency tracking.
  • Avoid unnecessary custom equality logic.
  • Do not convert every TypeScript property into a signal without a reactive reason.
  • Model one clear source of truth and derive other values from it.
  • Keep business rules inside services or appropriate state abstractions instead of placing all logic in templates.

Angular Signal API Quick Reference

APIPurposeWritable
signal()Create reactive source stateYes
set()Replace a writable signal valueYes
update()Calculate a new value from the current valueYes
asReadonly()Expose writable state as read-onlyNo
computed()Create derived stateNo
effect()React to changes with side effectsN/A
untracked()Read without creating a dependencyN/A
linkedSignal()Create writable dependent stateYes

Final Mental Model

The easiest way to understand Angular Signals is to separate state, derivation, and effects.

Text
signal()
   |
   | stores source state
   ↓
computed()
   |
   | derives read-only state
   ↓
Template

When derived state must also be writable:

Text
Source Signal
     ↓
linkedSignal()
     ↓
Writable dependent state

When reactive state needs to interact with something outside the signal graph:

Text
Signal
   ↓
effect()
   ↓
localStorage / logging / third-party API / external behavior

For controlled state exposure:

Text
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.

Question Hint