| |

Angular 81 🅰️ State Management with Services

⏱️ Estimated read: 20 min | |

Every Angular application has state. A user is logged in. A shopping cart has items. A form has been partially filled. A theme is set to dark. Some state is local to a single component, and signal() or a component property is the right home for it. But state that is shared across components — the current user, the notification count, the loaded configuration — needs a place to live outside any single component. In Angular, that place is a service.

Services are not a state management library. They are a mechanism. A service is a class decorated with @Injectable that Angular instantiates once and shares with every component that injects it. When the service holds a signal, every component that reads that signal sees the same value. When the service updates the signal, every component that depends on it re-renders. This is the built-in Angular state management model, and for many applications it is sufficient. No Redux, no NgRx, no external library.

Key point: An Angular service provided in providedIn: 'root' is a singleton. One instance exists for the entire application. Any state stored in that service is shared by every consumer. The modern pattern is to store that state in signals, expose them as readonly, and mutate them only through methods. This gives you a service that behaves like a store — with the type safety of signals and the dependency injection of Angular .


Why services are the Angular state primitive

Angular’s dependency injection system is the oldest part of the framework. Services have been the mechanism for sharing logic and state since Angular 2. The addition of signals in Angular 17 made them the mechanism for sharing reactive state as well.

The component-local problem. A component that holds state in a property owns that state. It can pass the state down to child components via inputs. It can emit changes up via outputs. But it cannot share the state with a sibling component or a component in a different branch of the tree. For that, the state must live in a service that both components inject.

The prop-drilling problem. An alternative is to pass the state down through every intermediate component. This works for shallow trees and becomes painful for deep ones. A service removes the intermediate components from the chain entirely. Any component that needs the state injects the service directly.

The duplication problem. Without a service, two components that need the same piece of derived state — “is the user logged in?” — must each compute it. If the logic changes, both components must change. A service computes it once and both components read the result.

The testing problem. State in a service can be replaced with a mock in tests. TestBed provides the mock in place of the real service, and the component under test sees the mock’s state. State in component properties cannot be replaced this way; the component owns its own state, and the test must manipulate it through the component’s API.

The trade-off. A root-provided service is a singleton. It lives for the entire application lifetime. Every piece of shared state accumulates in the same set of services. As the application grows, the services grow with it. Without discipline, a handful of root services become a tangle of cross-referencing signals and effects. The discipline is to keep services focused: one service per domain, and within each service, one signal per concept. When a service becomes too large, it is a sign that the domain should be split.


a. The Signal-Based Service Pattern

The modern pattern for a state service uses signals to hold the state, readonly to prevent external mutation, and methods to expose the mutations.

// cart.service.ts
import { Injectable, signal, computed } from '@angular/core';

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

@Injectable({ providedIn: 'root' })
export class CartService {
  private _items = signal<CartItem[]>([]);

  readonly items = this._items.asReadonly();
  readonly total = computed(() =>
    this._items().reduce((sum, item) => sum + item.price * item.quantity, 0)
  );
  readonly count = computed(() =>
    this._items().reduce((sum, item) => sum + item.quantity, 0)
  );

  addItem(item: Omit<CartItem, 'quantity'>): void {
    this._items.update(items => {
      const existing = items.find(i => i.id === item.id);
      if (existing) {
        return items.map(i =>
          i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
        );
      }
      return [...items, { ...item, quantity: 1 }];
    });
  }

  removeItem(id: string): void {
    this._items.update(items => items.filter(i => i.id !== id));
  }

  clear(): void {
    this._items.set([]);
  }
}

The private _items signal holds the raw state. The public items property is a readonly view. Components read cart.items() to get the array, but they cannot call cart.items.set() because the readonly wrapper removes the setter. Mutations go through the addItem, removeItem, and clear methods, which use signal.update() to produce new arrays rather than mutating the existing one.

The total and count properties are computed signals. They derive their values from the items signal. When items changes, Angular knows that total and count are out of date and recomputes them on the next read. This is the reactive graph in action: a change to one signal propagates to everything that depends on it.

A component consumes the service by injecting it and reading the signals in its template:

@Component({
  selector: 'cart-display',
  template: `
    <p>Items: {{ cart.count() }}</p>
    <p>Total: {{ cart.total() | currency }}</p>
    @for (item of cart.items(); track item.id) {
      <div>{{ item.name }} — {{ item.price | currency }}</div>
    }
  `,
})
export class CartDisplayComponent {
  cart = inject(CartService);
}

The component does not subscribe to anything. It does not implement OnDestroy. The signals handle change detection. When cart.addItem() is called from anywhere in the application, every component reading cart.items(), cart.total(), or cart.count() re-renders.


b. The Store Pattern with Facade Services

As applications grow, a single service holding all the state for a domain becomes unwieldy. The next step is to split the state into a store — a signal that holds the state — and a facade — a service that exposes methods for reading and mutating the state.

The store is a plain function or class that creates the state and the mutations. The facade is the service that injects the store, exposes readonly signals, and adds methods like data loading and error handling.

// user.store.ts
import { Injectable, signal, computed } from '@angular/core';

export interface User {
  id: string;
  name: string;
  email: string;
}

@Injectable({ providedIn: 'root' })
export class UserStore {
  private _users = signal<User[]>([]);
  private _loading = signal(false);
  private _error = signal<string | null>(null);

  readonly users = this._users.asReadonly();
  readonly loading = this._loading.asReadonly();
  readonly error = this._error.asReadonly();

  readonly userCount = computed(() => this._users().length);
  readonly hasError = computed(() => this._error() !== null);

  setUsers(users: User[]): void {
    this._users.set(users);
  }

  addUser(user: User): void {
    this._users.update(users => [...users, user]);
  }

  removeUser(id: string): void {
    this._users.update(users => users.filter(u => u.id !== id));
  }

  setLoading(value: boolean): void {
    this._loading.set(value);
  }

  setError(message: string | null): void {
    this._error.set(message);
  }
}

The facade service injects the store and adds the data-fetching logic:

// user.facade.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { UserStore, User } from './user.store';

@Injectable({ providedIn: 'root' })
export class UserFacade {
  private store = inject(UserStore);
  private http = inject(HttpClient);

  readonly users = this.store.users;
  readonly loading = this.store.loading;
  readonly error = this.store.error;
  readonly userCount = this.store.userCount;

  async loadUsers(): Promise<void> {
    this.store.setLoading(true);
    this.store.setError(null);
    try {
      const users = await firstValueFrom(this.http.get<User[]>('/api/users'));
      this.store.setUsers(users);
    } catch (error) {
      this.store.setError('Failed to load users');
    } finally {
      this.store.setLoading(false);
    }
  }

  async addUser(user: Omit<User, 'id'>): Promise<void> {
    const created = await firstValueFrom(this.http.post<User>('/api/users', user));
    this.store.addUser(created);
  }

  removeUser(id: string): void {
    this.store.removeUser(id);
  }
}

Components inject the facade, not the store. They read the readonly signals and call the methods. The store’s mutation methods are never exposed to the component layer. The facade is the API.

This separation is not required for small applications. It becomes valuable when the same store is used by multiple facades — a UserFacade for the user list and a UserDetailFacade for a single user’s details — or when the loading and error logic is complex enough to warrant its own class. The pattern also makes testing easier: the store can be tested without HTTP, and the facade can be tested with a mocked store.


c. Component and Route-Level Service Scoping

Not every service should be a singleton. Angular’s DI system supports scoping a service to a component or a route. The scope determines how long the service instance lives and which components share it.

A service provided in providedIn: 'root' is a singleton for the application. A service provided in a component’s providers array is created when the component is instantiated and destroyed when the component is destroyed. Every child component of that component shares the same instance. A service provided in a route’s providers array is created when the route is activated and destroyed when the route is deactivated.

@Component({
  selector: 'editor',
  providers: [EditorStateService],
  template: '<editor-canvas /><editor-toolbar />',
})
export class EditorComponent {
  state = inject(EditorStateService);
}

The EditorStateService is provided in the EditorComponent‘s providers array. It is created when EditorComponent is instantiated. Both EditorCanvasComponent and EditorToolbarComponent inject the same instance because they are children of EditorComponent. When EditorComponent is destroyed, the service is destroyed with it.

This is the correct scope for state that belongs to a single feature and should not leak to the rest of the application. An editor’s state, a form’s draft state, and a wizard’s step state are all examples. When the user navigates away from the editor, the editor’s state is discarded. When they return, a new instance is created with fresh state.

Route-level providers work the same way but are declared on the route configuration:

export const routes: Routes = [
  {
    path: 'editor/:id',
    loadComponent: () => import('./editor.component').then(m => m.EditorComponent),
    providers: [EditorStateService],
  },
];

The service is created when the route activates and destroyed when the route deactivates. This is preferred over component-level providers when the service is needed by multiple components in the routed feature, because the route is the natural boundary of the feature.

For services that should be shared across the entire application — authentication state, user preferences, notification count — providedIn: 'root' is the correct scope. The rule is simple: root for application-wide state, route for feature state, component for state that is local to a component tree and should be discarded when the component is destroyed.


Complete Example Session

This session builds a signal-based cart service, a store-and-facade pattern for users, and a component-scoped editor service.

// ============================================
// PART 1: THE SIGNAL-BASED SERVICE
// ============================================

// cart.service.ts
import { Injectable, signal, computed } from '@angular/core';

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

@Injectable({ providedIn: 'root' })
export class CartService {
  private _items = signal<CartItem[]>([]);

  readonly items = this._items.asReadonly();
  readonly total = computed(() =>
    this._items().reduce((sum, item) => sum + item.price * item.quantity, 0)
  );
  readonly count = computed(() =>
    this._items().reduce((sum, item) => sum + item.quantity, 0)
  );
  readonly isEmpty = computed(() => this._items().length === 0);

  addItem(item: Omit<CartItem, 'quantity'>): void {
    this._items.update(items => {
      const existing = items.find(i => i.id === item.id);
      if (existing) {
        return items.map(i =>
          i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
        );
      }
      return [...items, { ...item, quantity: 1 }];
    });
  }

  removeItem(id: string): void {
    this._items.update(items => items.filter(i => i.id !== id));
  }

  updateQuantity(id: string, quantity: number): void {
    if (quantity <= 0) {
      this.removeItem(id);
      return;
    }
    this._items.update(items =>
      items.map(i => (i.id === id ? { ...i, quantity } : i))
    );
  }

  clear(): void {
    this._items.set([]);
  }
}

// ============================================
// PART 2: THE STORE
// ============================================

// user.store.ts
import { Injectable, signal, computed } from '@angular/core';

export interface User {
  id: string;
  name: string;
  email: string;
}

@Injectable({ providedIn: 'root' })
export class UserStore {
  private _users = signal<User[]>([]);
  private _loading = signal(false);
  private _error = signal<string | null>(null);

  readonly users = this._users.asReadonly();
  readonly loading = this._loading.asReadonly();
  readonly error = this._error.asReadonly();

  readonly userCount = computed(() => this._users().length);
  readonly hasError = computed(() => this._error() !== null);

  setUsers(users: User[]): void {
    this._users.set(users);
  }

  addUser(user: User): void {
    this._users.update(users => [...users, user]);
  }

  removeUser(id: string): void {
    this._users.update(users => users.filter(u => u.id !== id));
  }

  setLoading(value: boolean): void {
    this._loading.set(value);
  }

  setError(message: string | null): void {
    this._error.set(message);
  }
}

// ============================================
// PART 3: THE FACADE
// ============================================

// user.facade.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { UserStore, User } from './user.store';

@Injectable({ providedIn: 'root' })
export class UserFacade {
  private store = inject(UserStore);
  private http = inject(HttpClient);

  readonly users = this.store.users;
  readonly loading = this.store.loading;
  readonly error = this.store.error;
  readonly userCount = this.store.userCount;

  async loadUsers(): Promise<void> {
    this.store.setLoading(true);
    this.store.setError(null);
    try {
      const users = await firstValueFrom(this.http.get<User[]>('/api/users'));
      this.store.setUsers(users);
    } catch {
      this.store.setError('Failed to load users');
    } finally {
      this.store.setLoading(false);
    }
  }

  async addUser(user: Omit<User, 'id'>): Promise<void> {
    const created = await firstValueFrom(this.http.post<User>('/api/users', user));
    this.store.addUser(created);
  }

  removeUser(id: string): void {
    this.store.removeUser(id);
  }
}

// ============================================
// PART 4: THE COMPONENT THAT CONSUMES
// ============================================

// cart-display.component.ts
import { Component, inject } from '@angular/core';
import { CurrencyPipe } from '@angular/common';
import { CartService } from './cart.service';

@Component({
  selector: 'cart-display',
  imports: [CurrencyPipe],
  template: `
    @if (cart.isEmpty()) {
      <p>Your cart is empty.</p>
    } @else {
      <p>Items: {{ cart.count() }}</p>
      <p>Total: {{ cart.total() | currency }}</p>
      @for (item of cart.items(); track item.id) {
        <div>
          {{ item.name }} — {{ item.price | currency }} × {{ item.quantity }}
          <button (click)="cart.removeItem(item.id)">Remove</button>
        </div>
      }
    }
  `,
})
export class CartDisplayComponent {
  cart = inject(CartService);
}

// ============================================
// PART 5: THE COMPONENT-SCOPED SERVICE
// ============================================

// editor-state.service.ts
import { Injectable, signal, computed } from '@angular/core';

@Injectable()
export class EditorStateService {
  private _content = signal('');
  private _isDirty = signal(false);

  readonly content = this._content.asReadonly();
  readonly isDirty = this._isDirty.asReadonly();
  readonly canSave = computed(() => this._isDirty() && this._content().length > 0);

  updateContent(value: string): void {
    this._content.set(value);
    this._isDirty.set(true);
  }

  markSaved(): void {
    this._isDirty.set(false);
  }

  reset(): void {
    this._content.set('');
    this._isDirty.set(false);
  }
}

// ============================================
// PART 6: THE COMPONENT WITH SCOPED PROVIDER
// ============================================

// editor.component.ts
import { Component, inject } from '@angular/core';
import { EditorStateService } from './editor-state.service';

@Component({
  selector: 'editor',
  providers: [EditorStateService],
  template: `
    <textarea
      [value]="state.content()"
      (input)="onInput($event)"
    ></textarea>
    <button [disabled]="!state.canSave()" (click)="save()">Save</button>
    @if (state.isDirty()) {
      <span>Unsaved changes</span>
    }
  `,
})
export class EditorComponent {
  state = inject(EditorStateService);

  onInput(event: Event) {
    const target = event.target as HTMLTextAreaElement;
    this.state.updateContent(target.value);
  }

  save() {
    // persist the content
    this.state.markSaved();
  }
}

// ============================================
// PART 7: THE ROUTE-LEVEL PROVIDER
// ============================================

// app.routes.ts
import { Routes } from '@angular/router';
import { EditorStateService } from './editor/editor-state.service';

export const routes: Routes = [
  {
    path: 'editor/:id',
    loadComponent: () =>
      import('./editor/editor.component').then(m => m.EditorComponent),
    providers: [EditorStateService],
  },
];

// ============================================
// PART 8: THE COMPONENT THAT READS FROM MULTIPLE SERVICES
// ============================================

// checkout.component.ts
import { Component, inject, computed } from '@angular/core';
import { CartService } from './cart.service';
import { UserFacade } from './user.facade';

@Component({
  selector: 'checkout',
  template: `
    <h2>Checkout</h2>
    <p>Customer: {{ users.users()[0]?.name ?? 'Guest' }}</p>
    <p>Items: {{ cart.count() }}</p>
    <p>Total: {{ cart.total() | currency }}</p>
    <button [disabled]="cart.isEmpty()" (click)="submit()">Place order</button>
  `,
})
export class CheckoutComponent {
  cart = inject(CartService);
  users = inject(UserFacade);

  submit() {
    // collect data from both services
  }
}

// ============================================
// PART 9: THE TEST WITH MOCKED SERVICE
// ============================================

// cart-display.component.spec.ts
import { TestBed } from '@angular/core/testing';
import { CartDisplayComponent } from './cart-display.component';
import { CartService } from './cart.service';

describe('CartDisplayComponent', () => {
  it('shows the empty state when no items', () => {
    const mockCart = {
      items: () => [],
      count: () => 0,
      total: () => 0,
      isEmpty: () => true,
      addItem: () => {},
      removeItem: () => {},
      updateQuantity: () => {},
      clear: () => {},
    };

    TestBed.configureTestingModule({
      imports: [CartDisplayComponent],
      providers: [{ provide: CartService, useValue: mockCart }],
    });

    const fixture = TestBed.createComponent(CartDisplayComponent);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toContain('empty');
  });
});

// ============================================
// PART 10: THE PROVIDEDIN SCOPING MATRIX
// ============================================

// providedIn: 'root'            → singleton for the application
// provided in component          → per component instance
// provided in route              → per route activation
// provided in feature module     → per lazy-loaded module
//
// Rule of thumb:
//   app-wide state → root
//   feature state → route or component
//   ephemeral state → component

The ten parts cover the signal-based service, the store, the facade, the consuming component, the component-scoped service, the component with scoped provider, the route-level provider, a component reading from multiple services, the test with a mocked service, and the providedIn scoping matrix.


Quick Reference

The Service Patterns

PatternWhen to Use
Signal serviceApplication-wide state with computed derivations
Store + facadeFeature state with data loading, error, and loading states
Component-scopedState local to a component tree
Route-scopedState local to a feature route

The Signal API in Services

MethodPurpose
signal(initial)Create a writable signal
asReadonly()Expose a readonly view
computed(fn)Derive a value from other signals
update(fn)Mutate based on previous value
set(value)Replace the value

The Scoping Options

ScopeDeclared InLifetime
RootprovidedIn: 'root'Application lifetime
Componentproviders: [Service] on @ComponentComponent lifetime
Routeproviders: [Service] on route configRoute activation lifetime
Moduleproviders: [Service] on @NgModuleModule lifetime

The Common Anti-Patterns

Anti-PatternProblem
Public writable signalComponent can set the state directly
State in component for shared dataSibling components can’t read it
Root service for feature stateState persists after route change
Facade with no storeFacade becomes a god object

Best Practices

✅ Do This:

// Keep the writable signal private
private _items = signal<CartItem[]>([]);                       // ✅
// Expose readonly views
readonly items = this._items.asReadonly();                     // ✅
// Mutate through methods, not direct set
addItem(item: CartItem) { this._items.update(...); }          // ✅
// Use computed for derived state
readonly total = computed(() => ...);                          // ✅
// Scope feature state to the route or component
providers: [EditorStateService]                                // ✅

❌ Don’t Do This:

// Don't expose the writable signal
readonly items = signal<CartItem[]>([]);  // writable              // ❌
// Don't mutate arrays in place
this._items().push(item);  // no trigger                          // ❌
// Don't put feature state in root if it should expire
@Injectable({ providedIn: 'root' })
export class EditorStateService {}                              // ❌
// Don't store derived state as a separate signal
total = signal(0);  // must be kept in sync manually             // ❌
// Don't inject the store directly into components
// Inject the facade instead.                                    // ❌

Common Pitfalls

PitfallWhy It HappensFix
State doesn’t updateMutating the array in placeUse signal.update() with new array
Component reads stale valueReading a set value outside a signalRead the signal in the template
Feature state persistsProvided at rootMove to route or component providers
Total is out of syncTotal stored separatelyUse computed()
Test fails to find serviceProvider not configuredAdd to TestBed providers
Service not singletonProvided in two placesProvide in only one scope

Real-World Examples

1. Signal-Based Cart

private _items = signal<CartItem[]>([]);
readonly items = this._items.asReadonly();

2. Computed Total

readonly total = computed(() => this._items().reduce(...));

3. Store with Loading

private _loading = signal(false);
readonly loading = this._loading.asReadonly();

4. Facade Method

async loadUsers(): Promise<void> { ... }

5. Component-Scoped Provider

@Component({ providers: [EditorStateService] })

6. Route-Level Provider

{ path: 'editor', providers: [EditorStateService] }

7. Readonly Signal in Component

cart = inject(CartService);
// cart.items() is readonly

8. Update Through Method

addItem(item: Omit<CartItem, 'quantity'>) { this._items.update(...); }

9. Mock in Test

providers: [{ provide: CartService, useValue: mockCart }]

10. Computed from Multiple Signals

readonly canSave = computed(() => this._isDirty() && this._content().length > 0);

Visual

The Service State Flow

┌──────────────────────────────────────────────┐
│  SERVICE STATE FLOW                          │
│                                              │
│  CartService                                 │
│  ┌────────────────────────────────────────┐  │
│  │ private _items = signal([])             │  │
│  │ readonly items = _items.asReadonly()    │  │
│  │ readonly total = computed(...)          │  │
│  │ readonly count = computed(...)          │  │
│  │                                         │  │
│  │ addItem(), removeItem(), clear()        │  │
│  └────────────────────────────────────────┘  │
│       │                                      │
│       ├──> Component A reads items()         │
│       ├──> Component B reads total()         │
│       └──> Component C reads count()         │
│                                              │
│  All components see the same state.          │
│  Any mutation re-renders all of them.        │
│                                              │
└──────────────────────────────────────────────┘

The Store and Facade Pattern

┌──────────────────────────────────────────────┐
│  STORE + FACADE                              │
│                                              │
│  UserStore (state only):                     │
│    ├─ private _users, _loading, _error       │
│    ├─ readonly users, loading, error         │
│    └─ setUsers(), setLoading(), setError()   │
│                                              │
│  UserFacade (orchestration):                 │
│    ├─ inject UserStore                       │
│    ├─ inject HttpClient                      │
│    ├─ expose readonly signals                │
│    └─ loadUsers(), addUser()                 │
│                                              │
│  Component:                                  │
│    └─ inject UserFacade, read signals        │
│                                              │
│  The component never sees the store.         │
│                                              │
└──────────────────────────────────────────────┘

The Scoping Decision

┌──────────────────────────────────────────────┐
│  SCOPING DECISION                            │
│                                              │
│  Is the state used across the entire app?    │
│    └─ YES → providedIn: 'root'               │
│                                              │
│  Is it used only inside one feature route?   │
│    └─ YES → providers on route               │
│                                              │
│  Is it used by one component tree and        │
│  should be discarded when that tree dies?    │
│    └─ YES → providers on component           │
│                                              │
│  The narrower the scope, the sooner the      │
│  state is released.                          │
│                                              │
└──────────────────────────────────────────────┘

The Testing Pattern

┌──────────────────────────────────────────────┐
│  TESTING WITH MOCKED SERVICE                 │
│                                              │
│  TestBed.configureTestingModule({            │
│    imports: [Component],                     │
│    providers: [                              │
│      {                                       │
│        provide: CartService,                 │
│        useValue: mockCart                    │
│      }                                       │
│    ],                                        │
│  });                                         │
│                                              │
│  The component injects the mock.             │
│  The test controls the mock's state.         │
│  No HTTP, no real service, no setup.         │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Root serviceprovidedIn: 'root', singleton
Component serviceproviders on component, per instance
Route serviceproviders on route, per activation
Signal statesignal() for writable, asReadonly() for view
Derived statecomputed()
Mutationupdate() or set() through methods
Store patternState class with setters
Facade patternService that orchestrates store and HTTP
TestingTestBed with mock provider

Key takeaways:

  • A service provided in providedIn: 'root' is a singleton for the application. Every component that injects it gets the same instance, and any state stored in it is shared. This is the foundation of Angular’s built-in state management .
  • Signals are the modern way to hold reactive state in a service. A signal() holds the value, asReadonly() exposes it for reading, and computed() derives secondary values. Mutations go through methods that use update() or set() .
  • The store-and-facade pattern separates state from orchestration. The store holds the signals and the mutation methods. The facade injects the store, adds HTTP calls and error handling, and exposes only the readonly signals and the public methods .
  • Scope the service to the lifetime of the state. Application-wide state goes in providedIn: 'root'. Feature state goes in route providers. State that should be discarded when a component is destroyed goes in component providers .
  • Never expose the writable signal. _items is private. items is this._items.asReadonly(). Components read the signal but cannot call set() on it. Mutations go through the service’s methods .
  • Use computed() for derived state, not separate signals. A total signal that must be manually updated will drift out of sync. A computed() recalculates automatically when its dependencies change .
  • Test with mock providers. TestBed.configureTestingModule({ providers: [{ provide: CartService, useValue: mockCart }] }) replaces the real service with a mock. The component under test reads the mock’s state, and the test controls the mock entirely.

Remember: Angular does not need a state management library for most applications. Services are the state management. A root-provided service with signals is a store. A service that wraps a store and adds HTTP is a facade. A component-scoped provider is feature state that expires when the feature closes. The discipline is to keep the writable signals private, expose them as readonly, mutate only through methods, and derive computed values instead of storing them. This is the Angular state model: dependency injection for distribution, signals for reactivity, and scoping for lifetime.

Was this helpful?


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!