Free discovery callFree discovery call

SignalState: The New Standard for Component State in Angular

SignalState makes managing component state in Angular easier with @ngrx/signals. Less boilerplate, better performance—see how it works!

DevelopmentLast updated: 25 Feb 20254 min read

By Matej Jakšić

Angular's state management is evolving. For years, we have relied on the "Service with a Subject" pattern or @ngrx/component-store to manage component state.

Now, with the release of  SignalState, Angular developers have a new way to handle component state that leverages Angular’s native Signals API. The NgRx team recommends @ngrx/signals as the preferred alternative to @ngrx/component-store.

To manage global state, we can use SignalStore, which is also part of @ngrx/signals, however this will be explained in another blog post.

What are signals, anyway?

A signal is a wrapper around a value that notifies interested consumers when that value changes.

Instead of manually subscribing to changes or managing streams with RxJS, signals automatically track state updates and trigger re-renders when their values change. With signals, you have less boilerplate code and your components become more declarative.

What makes @ngrx/signals special?

Unlike native Angular signals, @ngrx/signals is a dedicated state management solution.

It bundles powerful utilities like signalState and patchState—which help you define and update your component’s state.

💡
The idea is simple: instead of scattering state logic throughout your component, you centralize it in one place.

This not only makes your code cleaner but also easier to maintain and debug.

Getting started with @ngrx/signals

Installation

To begin using @ngrx/signals, install the package via ng-add

ng add @ngrx/signals@latest

Or with npm/yarn/pnpm:

npm install @ngrx/signals

Basic component example

Let’s create a simple todo component to see signals in action. The code below defines a signal state with two todos, computed signal for todos counter, and addToDo() method that adds a new, unique todo:

import { Component } from '@angular/core';
import { signalState, patchState, computed } from '@ngrx/signals';

interface Todo {
  id: number;
  title: string;
  content: string;
}

@Component({
  selector: 'app-todo',
  template: `
    <ng-container>
      <h2>Todos (Total: {{ todoCount() }})</h2>
      <ul>
        <!-- Using @for with track to improve rendering performance -->
        @for="let todo of state() track todo.id">
          <li>
            <h3>{{ todo.title }}</h3>
            <p>{{ todo.content }}</p>
          </li>
        </ul>
      <button (click)="addTodo()">Add Todo</button>
    </ng-container>
  `,
})
export class TodoComponent {
  state = signalState<Todo[]>([
    { id: 1, title: 'Shopping List', content: 'Eggs' },
    { id: 2, title: 'Work', content: 'Finish NgRx tutorial' },
  ]);

  // Get total number of todos
  todoCount = computed(() => this.state().length);

  // Function to add a new Todo to the state
 addTodo() {
  const newId = this.state().length + 1;
  patchState(this.state, { 
    todos: [...this.state(), { id: newId, title: `New Todo ${newId}`, content: 'Some new task', completed: false }]
  });
}
}

import { Component } from '@angular/core';
import { signalState, patchState, computed } from '@ngrx/signals';

interface Todo {
  id: number;
  title: string;
  content: string;
}

@Component({
  selector: 'app-todo',
  template: `
    <ng-container>
      <h2>Todos (Total: {{ todoCount() }})</h2>
      <ul>
        <!-- Using @for with track to improve rendering performance -->
        @for="let todo of state() track todo.id">
          <li>
            <h3>{{ todo.title }}</h3>
            <p>{{ todo.content }}</p>
          </li>
        </ul>
      <button (click)="addTodo()">Add Todo</button>
    </ng-container>
  `,
})
export class TodoComponent {
  state = signalState<Todo[]>([
    { id: 1, title: 'Shopping List', content: 'Eggs' },
    { id: 2, title: 'Work', content: 'Finish NgRx tutorial' },
  ]);

  // Get total number of todos
  todoCount = computed(() => this.state().length);

  // Function to add a new Todo to the state
 addTodo() {
  const newId = this.state().length + 1;
  patchState(this.state, { 
    todos: [...this.state(), { id: newId, title: `New Todo ${newId}`, content: 'Some new task', completed: false }]
  });
}
}

Instead of creating additional signal or a new object in the state, we used computed signal which derives its value from other signals.

Benefits of using computed:

  • Reactivity: The computed function allows you to derive values (like the count of todos) based on the state without explicitly managing or mutating the state.
  • Automatic updates: The todoCount() value automatically updates whenever the state changes

State can be updated in multiple ways.

The patchState function offers a type-safe method for updating specific parts of your state. It accepts a SignalState or SignalStore instance as its first argument, followed by partial states or state updaters as additional arguments.

// Providing a partial state object.
addTodo() {
  const newId = this.state().length + 1;
  patchState(this.state, { 
    todos: [...this.state(), { id: newId, title: `New Todo ${newId}`, content: 'Some new task', completed: false }]
  });
}

// Providing a partial state updater. 
addTodo() {
  const newId = this.state().length + 1;
  patchState(this.state, (todos) => [
    ...todos,
    { id: newId, title: `New Todo ${newId}`, content: 'Some new task', completed: false },
  ]);
}

// Providing a sequence of partial state objects and/or updaters.
addTodo() {
  const newId = this.state().length + 1;
  patchState(
    this.state,
    { todos: [...this.state(), { id: newId, title: `New Todo ${newId}`, content: 'Some new task', completed: false }] },
    (todos) => [...todos]
  );
}

Custom state updaters

A custom state updater is a function that returns a state update operation. This operation can be a partial update to the state, and is typically used when you want to modify a specific part of the state without affecting the rest of the state.

 // Dynamic update for Todo title or content
  setTodoProperty(id: number, property: keyof Todo, value: any): PartialStateUpdater<Todo[]> {
    return (state) => state.map(todo =>
      todo.id === id ? { ...todo, [property]: value } : todo
    );
  }

  // Function to update the title of a todo
  changeTodoTitle(id: number, title: string) {
    patchState(this.state, this.setTodoProperty(id, 'title', title));
  }

  // Function to update the content of a todo
  changeTodoContent(id: number, content: string) {
    patchState(this.state, this.setTodoProperty(id, 'content', content));
  }

Replacing @ngrx/component-store with @ngrx/signals

For many Angular projects, @ngrx/component-store has been the go-to solution for local state management.

However, with the introduction of @ngrx/signals, for the new projects—and even when refactoring existing ones—developers are encouraged to adopt @ngrx/signals to replace @ngrx/component-store for managing component state.

This change is driven by signals’ ability to simplify state updates and improve performance by triggering re-renders only when necessary.

Performance and developer experience

Thanks to signals reactive nature, only UI elements dependent on a specific signal update when that signal's value changes.

This targeted approach enhances performance, particularly in apps with frequent or granular state changes. The simple API also reduces complexity, which means developers can focus on building features instead of managing complex state infrastructure.

Conclusion

SignalState offers a fresh, efficient way to manage component state in Angular applications.

By leveraging Angular’s new Signals API, it reduces boilerplate while providing a powerful, reactive model for state management. For developers used to the traditional @ngrx/component-store, this approach provides an excellent migration path to a more modern architecture.

Try integrating @ngrx/signals into your next Angular project and experience the benefits of reactive state management firsthand.

Happy coding! 🥳

Thank you for your the time to read this blog! Feel free to share your thoughts about this topic and drop us an email at hello@prototyp.digital.

Related ArticlesTechnology x Design

View all articlesView all articles
( 01 )Get started

Start a Project