MiniRx Signal Store is currently under active development. You can follow the progress through the links below:

What Can You Anticipate from MiniRx Signal Store?

  • Signal Store is a state management library built exclusively for Angular.
  • It fully integrates Angular Signals and utilizes Modern Angular APIs under the hood.
  • The library follows the same core principles as the original MiniRx Store:
    • Handle global state on a large scale with the Store (Redux) API.
    • Handle global state with minimal boilerplate using Feature Stores.
    • Handle local component state with Component Stores.
    • MiniRx consistently aims for the optimal balance between power, simplicity, and lightweight design.
  • Signal Store adopts and advocates for modern Angular best practices:
    • Signals handle all (synchronous) state.
    • RxJS manages events and asynchronous operations.
  • It simplifies working with both RxJS and Signals; for instance, the connect and rxEffect methods work with both Signals and Observables.
  • Easy migration path: transitioning from MiniRx Store to Signal Store is straightforward — update your TypeScript imports, and remove the async pipes and non-null assertions (!) from your templates.

Now, let’s dive into the details of the Signal Store API.

The majority of the API surface closely mirrors the original MiniRx Store. This post will highlight the additions and modifications.

Component Store and Feature Store

Note The Feature Store and Component Store share the same API. Their primary differences lie in their internal mechanics and typical use cases.

We’ll use the Component Store for the upcoming examples, but the API changes are identical for the Feature Store.

All code samples are available in this StackBlitz project.

select

Use the select method to read state from your Component Store.

As you might have guessed, the select method now returns an Angular Signal.

Here's an example:

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

@Component({
// ...
})
export class SelectDemoComponent {
  private cs = createComponentStore({counter: 1});
  doubleCounter: Signal<number> = this.cs.select(state => state.counter * 2)
}
Enter fullscreen mode Exit fullscreen mode

You can use the Signal directly in your template like this:

<pre>
  doubleCount: {{doubleCounter()}},
</pre>  
Enter fullscreen mode Exit fullscreen mode

The select method is available in the Store, Feature Store, and Component Store.

StackBlitz demo: SelectDemoComponent

setInitialState

We've removed the setInitialState method from the Feature Store and Component Store. You must now provide an initialState upfront, which aligns more closely with the behavior of native Angular Signals.

connect

The new connect method is here! It lets you **connect** your store to external sources like Observables and Signals, helping establish your store as the Single Source of Truth.

Note setState no longer accepts an Observable; use connect for that scenario.

Here’s how it works:

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

interface State {
  counterFromObservable: number;
  counterFromSignal: number;
}

@Component({
// ...
})
export class ConnectDemoComponent {
  cs: ComponentStore<State> = createComponentStore<State>({
    counterFromObservable: 0,
    counterFromSignal: 0,
  });

  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.cs.connect({
      counterFromObservable: observableCounter$, // Observable
      counterFromSignal: signalCounter, // Signal
    });

    setInterval(() => signalCounter.update((v) => v + 1), interval);
  }
}
Enter fullscreen mode Exit fullscreen mode

Then, you can access these Signals in your template:

<!-- Access top level state properties easily from the cs.state Signal -->
<ng-container *ngIf="cs.state() as state">
  <pre>
    counterFromRxJS: {{ state.counterFromObservable }}, 
    counterFromSignal: {{ state.counterFromSignal }}
  </pre>
</ng-container>
Enter fullscreen mode Exit fullscreen mode

StackBlitz demo: ConnectDemoComponent

rxEffect

We've renamed the effect method to rxEffect to prevent confusion with Angular's own Signal-based effect function.

The rxEffect method is available in Feature Store and Component Store for handling side effects like API calls.

It returns a function you can call later, optionally with a payload, to trigger the side effect.

Notice in this next example that the side effect can be triggered with a raw value, an Observable, or even a Signal:

import { Component, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { createComponentStore, tapResponse } from '@mini-rx/signal-store';
import { of, Observable, map, switchMap, delay } from 'rxjs';

function apiCall(filter: string): Observable<string[]> {
// ...
}

interface State {
  cities: string[];
}

@Component({
// ...
})
export class EffectDemoComponent {
  cs = createComponentStore<State>({
    cities: [],
  });

  private fetchCitiesEffect = this.cs.rxEffect<string>(
    switchMap((filter) => {
      return apiCall(filter).pipe(
        tapResponse({
          next: (cities) => this.cs.setState({ cities }),
          error: console.error,
        })
      );
    })
  );

  formControl = new FormControl();
  private filterChangeObservable$ = this.formControl.valueChanges; 
  private filterChangeSignal = signal('');

  constructor() {
    // Observable
    // Every emission of the Observable will trigger the API call
    this.fetchCitiesEffect(this.filterChangeObservable$);
    // Signal
    // The Signals initial value will immediately trigger the API call
    // Every new Signal value will trigger the API call
    this.fetchCitiesEffect(this.filterChangeSignal); 
  }

  triggerEffectWithSignal() {
    // Update the Signal value
    this.filterChangeSignal.set('');

    setTimeout(() => {
      this.filterChangeSignal.set('a');
    }, 1000);

    setTimeout(() => {
      this.filterChangeSignal.set('c');
    }, 2000);
  }

  triggerEffectWithRawValue() {
    // Trigger the API call with a raw value
    this.fetchCitiesEffect('Phi');
  }
}
Enter fullscreen mode Exit fullscreen mode

And in the component template:

<!-- Access top level state properties easily from the cs.state Signal -->
<ng-container *ngIf="cs.state() as state"> 
  <label>Trigger Effect with RxJS Observable (FormControl.valueChanges):</label>
  <input [formControl]="formControl" placeholder="Search city...">
  <pre>cities: {{state.cities | json}}</pre>  

  <button (click)="triggerEffectWithSignal()">Trigger Effect with Signal</button><br>
  <button (click)="triggerEffectWithRawValue()">Trigger Effect with Raw Value</button>
</ng-container>
Enter fullscreen mode Exit fullscreen mode

StackBlitz demo: EffectDemoComponent

Component Store Lifecycle

With Signal Store, you can instantiate a Component Store directly inside a component. The store will be automatically destroyed along with the component itself.

This is achieved by using Angular's DestroyRef internally within the Component Store.

The following example shows a child component with its own local Component Store, where the child is conditionally shown/hidden by its parent.

@Component({
  // ...
})
export class DestroyDemoChildComponent {
  // Create a local Component Store
  cs = createComponentStore({counter: 1}); 

  constructor() {
    // Connect a RxJS timer to the Component Store
    this.cs.connect({counter: timer(0, 1000).pipe(
      tap(v => console.log('timer emission:', v)) // We can see the logging WHILE the ChildComponent is visible (see the JS console)
    )})
  }
}
Enter fullscreen mode Exit fullscreen mode

Once the child component is removed, its Component Store is destroyed as well, and the store's cleanup logic runs, unsubscribing all internal subscriptions (including the one for the timer).

StackBlitz demo: DestroyDemoChildComponent

Immutable Signal State

Angular Signals give developers the flexibility to modify state directly, bypassing the guarded update or set mechanisms at any point in time.

This openness, however, can lead to unpredictable outcomes and introduce subtle bugs into an application.

To address this, the MiniRx Signal Store incorporates the ImmutableState Extension, a safeguard also present in the original MiniRx Store, which enforces immutability by design.

Whenever state is mutated inadvertently, the extension triggers a clear error message in the JavaScript console.

@Component({
// ...
})
export class ImmutableDemoComponent {
  private signalState = signal({counter: 1});
  counterFromSignal = computed(() => this.signalState().counter);

  cs = createComponentStore({counter: 1}, {
    extensions: [new ImmutableStateExtension() // FYI you could add extensions globally with `provideComponentStoreConfig` in main.ts
  ]});
  counterFromComponentStore = this.cs.select(state => state.counter);

  // SIGNAL
  // valid state update
  incrementSignalCounter() {
    this.signalState.update(state => ({...state, counter: state.counter + 1}))
  }

  // Signal Mutations
  // no error, you are entering danger zone, without knowing it
  mutateSignalA() {
    this.signalState().counter = 666;
  }

  mutateSignalB() {
    this.signalState.update(state => {
      state.counter = 666;
      return state;
    })
  }

  // COMPONENT STORE
  // valid state update
  incrementComponentStoreCounter() {
    this.cs.setState(state => ({counter: state.counter + 1}))
  }

  // Component Store Signal Mutations
  // As expected, mutating state will throw an error
  mutateComponentStoreSignalA() {
    this.cs.state().counter = 666; 
  }

  mutateComponentStoreSignalB() {
    this.cs.setState(state => {
      state.counter = 666;
      return state;
    })
  }
}

You can experiment with this behavior in the StackBlitz demo: ImmutableDemoComponent

Memoized Signal Selectors

Three factory functions—createSelector, createFeatureStateSelector, and createComponentStateSelector—all produce a SignalSelector function. These selector functions accept a Signal as input and return another Signal as output.

The select method, available on the Store, Feature Store, and Component Store, accepts these Signal Selectors directly.

Signal Selectors are built with memoization in mind, ensuring that the projector function is not re-executed unnecessarily and computations remain efficient.

An interesting detail: the implementation leans on Angular's native Signal computed feature.

Consider this example, which pulls state from the Redux Store:

import { Component, inject, Signal } from "@angular/core";
import { createFeatureStateSelector, createSelector, Store } from "@mini-rx/signal-store";
import { Todo, TodosState } from "./todo-state";

// Memoized SignalSelectors
const getFeature = createFeatureStateSelector<TodosState>('todos');
const getTodos = createSelector(getFeature, state => state.todos);
const getTodosDone = createSelector(getTodos, todos => todos.filter(item => item.isDone))
const getTodosNotDone = createSelector(getTodos, todos => todos.filter(item => !item.isDone))

@Component({
// ...
})
export class MemoizedSignalSelectorsDemoComponent {
  private store = inject(Store); // Store is provided in the main.js file
  todosDone: Signal<Todo[]> = this.store.select(getTodosDone);
  todosNotDone: Signal<Todo[]> = this.store.select(getTodosNotDone);
}

Here’s how those Signals are accessed inside a component template:

<pre>DONE: {{ todosDone() | json }}</pre>
<pre>NOT DONE: {{ todosNotDone() | json }}</pre>

See it in action with the StackBlitz demo: MemoizedSignalSelectorsDemoComponent

Store (Redux)

Now, let's quickly overview the modifications made to the Store (Redux) API...

select

The select method serves as the primary way to retrieve state from the store, providing an Angular Signal as its output.

Revisiting the memoized selectors example demonstrates select in practice:

import { Component, inject, Signal } from "@angular/core";
import { createFeatureStateSelector, createSelector, Store } from "@mini-rx/signal-store";
import { Todo, TodosState } from "./todo-state";

@Component({
// ...
})
export class MemoizedSignalSelectorsDemoComponent {
  private store = inject(Store); // Store is provided in the main.js file
  todosDone: Signal<Todo[]> = this.store.select(getTodosDone);
  todosNotDone: Signal<Todo[]> = this.store.select(getTodosNotDone);
}

createRxEffect

Most of the (Redux) Store effects API remains unchanged. A notable difference is that createEffect has been rebranded to createRxEffect. The new terminology makes it instantly clear that this method is intended for integration with RxJS Observables.

A short example, drawn from the Signal Store RFC, looks like this:

import {
    Actions,
    createRxEffect,
    mapResponse,
} from '@mini-rx/signal-store';
import { ofType } from 'ts-action-operators';

@Injectable()
export class ProductsEffects {
  constructor(private productService: ProductsApiService, private actions$: Actions) {}

  loadProducts$ = createRxEffect(
    this.actions$.pipe(
      ofType(load),
      mergeMap(() =>
        this.productService.getProducts().pipe(
          mapResponse(
            (products) => loadSuccess(products),
            (error) => loadFail(error)
          )
        )
      )
    )
  );
} 

Standalone APIs

The MiniRx Signal Store embraces modern Angular standalone conventions.

Here's a concise list of the new APIs:

  • provideStore: Initializes the Redux Store, configuring reducers, metaReducers, and extensions
  • provideFeature: Introduces a feature state with its reducer, typically through the route configuration
  • provideEffects: Registers application effects, also typically via the route configuration
  • provideComponentStoreConfig: Applies a consistent configuration across all Component Stores

Note: For module-based applications, the established API remains available: StoreModule.forRoot(), StoreModule.forFeature(), EffectsModule.register(), and ComponentStoreModule.forRoot().

Feedback

We're excited about the upcoming Signal Store and hope it meets your expectations!

If you spot any area that could be refined or where a different approach might work better, don't hesitate to share your thoughts in the comments.

Your input is also welcome through the RFC discussion or the related Pull Request on GitHub, where you can play an active role in its development.

Thanks

A heartfelt thank you to those who took the time to review this article: