From RxJS to Signals: The Motivation Behind MiniRx Signal Store

Angular 16 shipped with a brand-new reactive primitive: Signal.

The arrival of Angular Signals is arguably one of the most significant shifts the framework has seen in years:

  • Is RxJS obsolete now?
  • Should Signals replace RxJS across the board?
  • Can Signals and RxJS Observables work together?
  • What are the new Angular best practices?

Driven by these questions, MiniRx began experimenting with Signals to explore what modern Angular state management should look like.

That exploration produced a new Signal-first state management package:

MiniRx Signal Store

  • Signal Store is a state management solution built for Angular-only applications
  • Signal Store relies on Angular Signals and uses modern Angular APIs under the hood
  • Signal Store encourages and encodes new Angular best practices:
    • Signals handle your synchronous state
    • RxJS takes care of events and asynchronous operations
  • Signal Store bridges the gap between RxJS and Signals: methods like connect and rxEffect accept both Signals and Observables interchangeably
  • Signal Store builds on the same proven foundation as the original (RxJS-based) MiniRx Store
    • It is an all-in-one package covering global and local state, from trivial to complex scenarios
    • You get three clearly separated state containers: Store (Redux), Feature Store, and Component Store
    • Maximum flexibility: working on a project that mixes simple and complex features? You can pick the ideal state container for each feature independently.

Getting Started

Requirements

  • Angular >= 16
  • RxJS >= 7.4.0

Install

Pull down the @mini-rx/signal-store package using your preferred package manager:

npm install @mini-rx/signal-store

API documentation

The full MiniRx Signal Store API reference lives in the README.

Modern Angular Best Practices in MiniRx Signal Store

The MiniRx Signal Store is built entirely with new Angular best practices.
Along the way, the library also advocates for these same practices in its public API:

  • Signals handle your synchronous state
  • RxJS handles events and asynchronous operations

Let's dig deeper into why this split makes sense...

Signals

Angular 16 gave us Signals as a brand-new reactive primitive. Up until then, RxJS was the dominant approach for reactive state management.

Why Signals?

Angular Signals bring a few clear benefits over RxJS:

  • No more manual subscriptions — and no dependency on the async pipe either
  • Lower learning curve (no pipe, no operators, and Signals are always synchronous)
  • Deriving state from other Signals via computed is simpler than working with RxJS combineLatest
  • Signals open the door to more granular and efficient Angular Change Detection in the future

Signals in MiniRx Signal Store

It's hard to argue against Signals as the new go-to for state!

MiniRx Signal Store commits fully to that idea: it uses Angular Signal internally and offers Signals throughout its public API:

  • The global state object for the Redux Store (shared by the Feature Store) is stored as an Angular Signal
  • In the public API, you'll find that every state container has a select method, which returns an Angular Signal
  • Memoized selectors pulled from the global state object are built with Angular computed

RxJS

You might wonder: Do Signals make RxJS obsolete?

For state management, the answer is yes — it is time to move on from BehaviorSubject!

However, RxJS remains invaluable when it comes to events and asynchronous workflows.

Distinct events with RxJS Subject

Signals fall short for event handling, as events can easily be missed. Consider this example with Angular's effect:

Introducing MiniRx Signal Store — figure 1

StackBlitz

One might assume that every state change would be logged inside effect, but that is not what happens when Signal state updates are synchronous...

An RxJS Subject proves to be the superior choice: it delivers all events, including those that occur synchronously. Take a look at this demonstration:

Introducing MiniRx Signal Store — figure 2

StackBlitz

Side effects and race-conditions

With RxJS-based streams, initiating side effects such as API calls becomes straightforward. Race conditions can be managed effectively using RxJS flattening operators (mergeMap, switchMap, concatMap, exhaustMap).

More operators

The possibilities are endless! RxJS provides over 100 operators for stream manipulation.
In practice, however, a modest set will cover most needs: debounceTime, distinctUntilChanged, map, filter, catchError, and others.

RxJS in MiniRx Signal Store

You can see the advantages of RxJS, right?

MiniRx Signal Store has embraced this: RxJS handles events and asynchronous tasks.

  • The Action stream within the (Redux) Store is an event stream, built on an RxJS Subject
  • Effects can pipe the Action stream to make API calls, using flattening operators to manage race-conditions
  • The rxEffect methods in Feature Store and Component Store rely on RxJS Subject

An interesting detail: the Component Store also integrates a compact Redux pattern, complete with its own Action stream based on RxJS Subject.

RxJS and Signal Interop

MiniRx Signal Store aims to simplify how you work with RxJS Observables and Signals.
The objective is to remove any need for conversion logic in your codebase, making toSignal and toObservable obsolete.

These MiniRx Signal Store APIs are designed to work with both Observables and Signals:

rxEffect

rxEffect enables side effects, like API calls, within Feature Stores and Component Stores.
There are three ways to kick off a side effect:

  • Using a raw value
  • Using a Signal
  • Using an Observable

This (Component Store) example uses an Angular Signal (Input) to initiate the API call:

import { Component, inject, input, Signal } from '@angular/core';
import { createComponentStore, tapResponse } from '@mini-rx/signal-store';
import { switchMap } from 'rxjs';
import { BookService } from '../book.service';

type State = {
  detail: BookDetail;
  isLoading: boolean;
}

const initialState: State = {
  detail: undefined,
  isLoading: false
}

@Component({
// ...
})
export class BookComponent {
  private store = createComponentStore(initialState);
  private bookService = inject(BookService);

  bookId = input.required<string>(); // Signal Input

  bookDetail: Signal<BookDetail> = this.store.select(state => state.detail);
  isLoading: Signal<boolean> = this.store.select(state => state.isLoading);

  // Create an Effect
  private loadDetail = this.store.rxEffect<string>(
    // Handle race-condition with switchMap
    switchMap(id => {
      this.store.setState({isLoading: true});

      return this.bookService.getBookDetail(id).pipe(
        tapResponse({
          next: (detail: BookDetail) => {this.store.setState({detail})},
          error: () => this.store.setState({isLoading: false})
        })
      )
    })
  )

  constructor() {
    // Fetch detail for every new bookId Signal value
    this.loadDetail(this.bookId)
  }
}
Enter fullscreen mode Exit fullscreen mode

connect

Available within Feature Store and Component Store.

The connect method allows you to link your store to external data sources, whether they are Observables or Signals.
This approach helps establish your store as the central source of truth for state.

Here we see a Component Store connected to both an Observable and a Signal:

import { Component, Signal, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { createComponentStore } from '@mini-rx/signal-store';
import { timer } from 'rxjs';

@Component({
// ...
})
export class ConnectComponent {
  store = createComponentStore({
    counter: 0,
    counterFromObservable: 0, // Will be updated via Observable
    counterFromSignal: 0, // Will be updated via Signal
  });

  sum: Signal<number> = this.store.select((state) => {
    return state.counter + state.counterFromObservable + state.counterFromSignal;
  });

  constructor() {
    const interval = 1000;

    const observableCounter$ = timer(0, interval); // Observable
    const signalCounter = signal(0); // Signal

    // Connect external sources (Observables or Signals) to the Component Store
    this.store.connect({
      counterFromObservable: observableCounter$, // Observable
      counterFromSignal: signalCounter, // Signal
    });

    setInterval(() => signalCounter.update((v) => v + 1), interval);
  }

  increment() {
    this.store.setState((state) => ({ counter: state.counter + 1 }));
  }
}
Enter fullscreen mode Exit fullscreen mode

All-in-one solution

MiniRx Signal Store comes with three distinct state containers ready to use:

The complete API is available in the README.

Flexibility

All three state containers work seamlessly together in an application.
You can pick the right container for each specific use-case.

Here are the most common scenarios:

Introducing MiniRx Signal Store — figure 3

Summary

These are promising times for Angular, as old patterns fade and new ones emerge.
You have seen MiniRx exploring the combination of Signals and RxJS, resulting in the Signal Store.

MiniRx Signal Store is a highly adaptable state management tool:
Whether you are dealing with global or local state, complex or simple, MiniRx Signal Store is equipped to handle it!

By combining flexibility with emerging Angular best practices, MiniRx Signal Store is ready to guide you through modern Angular development.

⭐ MiniRx on GitHub

Enjoying MiniRx? Please show your support with a GitHub star here.

Thank you! :)

Demos

MiniRx Signal Store has been validated in the following projects:

Release

MiniRx Signal Store 1.0.0 is now available!

Thanks

A big thank you goes out to the reviewer of this article: