MiniRx Signal Store 3 has been released (2025-01-27)!
What's new?
- Bugfix: make
createEffect
work with TypeScript 5.3 - Compatible with Angular 17+
- Internal refactorings and optimizations
npm i @mini-rx/signal-store
Welcome to MiniRx Signal Store, the new state management library from MiniRx.
- Signal Store is an Angular-only state management library
- Signal Store embraces Angular Signals and leverages Modern Angular APIs internally
- Signal Store is based on the same great concept as the original MiniRx Store
- Manage global state at large scale with the Store (Redux) API
- Manage global state with a minimum of boilerplate using Feature Stores
- Manage local component state with Component Stores
- All combined in a single library!
- Choose from feature to feature: use Redux, Feature Store/Component Store depending on the use-case
- MiniRx always tries to find the sweet spot between powerful, simple and lightweight!
- Signal Store implements and promotes new Angular best practices:
- Signals are used for (synchronous) state
- RxJS is used for events and asynchronous tasks
- Signal Store helps to streamline your usage of RxJS and Signals: e.g.
connect
andrxEffect
understand both Signals and Observables - Signal Store has first-class support for OOP style (e.g.
MyStore extends FeatureStore
), but offers also functional creation methods (e.g.createFeatureStore
) - Simple refactor: If you used MiniRx Store before, refactor to Signal Store will be straight-forward: change the TypeScript imports, remove the Angular async pipes (and ugly non-null assertions (
!
)) from the template
- Angular >= 17
- RxJS >= 7.8.1
To install the @mini-rx/signal-store package, use your package manager of choice:
npm install @mini-rx/signal-store
MiniRx Signal Store is highly flexible and offers three different well-defined state containers out of the box:
- Store (Redux)
- Feature Store
- Component Store
All three can be easily used together in your application. Depending on the use-case, you can choose the state container which suits your needs.
These are the typical use-cases:
The Redux pattern is great to manage state at large scale. MiniRx Signal Store offers a powerful Redux API.
- Actions: objects which describe events with an optional payload
- Reducers
- pure functions which know how to update state (based on the current state and a given action)
- reducers are run for every action in order to calculate the next state
- Effects: listen to a specific action, run side effects like API calls and handle race conditions (with RxJS flattening operators)
- Memoized selectors: pure functions which describe how to select state from the global state object
- Store
- holds the global state object
- wires everything up (reducers, effects)
- exposes the public Store API (
dispatch
,select
)
You define Actions like this:
import { action, payload } from 'ts-action';
import { Product } from '../models';
export const loadProducts = action('[Products] load');
export const loadProductsSuccess = action(
'[Products] load success',
payload<Product[]>(),
);
export const loadProductsError = action('[Products] load error');
export const deleteProduct = action(
'[Products] delete',
payload<{ id: number }>(),
);
FYI the powerful ts-action library is used to create actions with less boilerplate.
Defining a reducer looks like this:
import { on, reducer } from 'ts-action';
import { ProductsState } from './index';
import {
deleteProduct,
loadProductsSuccess,
} from './product.actions';
const initialState: ProductsState = {
list: [],
};
export const productReducer = reducer(
initialState,
on(loadProductsSuccess, (state, { payload }) => ({
...state,
list: payload,
})),
on(deleteProduct, (state, { payload }) => ({
...state,
list: state.list.filter((item) => item.id !== payload.id),
})),
);
FYI the powerful ts-action library is used to create reducers with less boilerplate.
You create an effect like this:
- Listen to a specific action
- Run an (in most cases) asynchronous task
- Return a new action, when the task succeeded/failed
import { inject, Injectable } from '@angular/core';
import {
Actions,
createRxEffect,
mapResponse,
} from '@mini-rx/signal-store';
import { ofType } from 'ts-action-operators';
import { ProductApiService } from '../product-api.service';
import { mergeMap } from 'rxjs';
import {
loadProducts,
loadProductsError,
loadProductsSuccess,
} from './product.actions';
@Injectable()
export class ProductEffects {
actions$ = inject(Actions);
todosApi = inject(ProductApiService);
loadTodos$ = createRxEffect(
this.actions$.pipe(
ofType(loadProducts),
mergeMap(() =>
this.todosApi.getTodos().pipe(
mapResponse(
(res) => loadProductsSuccess(res),
(err) => loadProductsError,
),
),
),
),
);
}
You can register reducers and effects with modern Angular standalone APIs (provideStore
, provideEffects
) when initializing the app:
import { ApplicationConfig } from '@angular/core';
import {
provideEffects,
provideStore,
ReduxDevtoolsExtension,
ImmutableStateExtension,
} from '@mini-rx/signal-store';
import { productReducer } from './products/state/product.reducer';
import { ProductEffects } from './products/state/product.effects';
export const appConfig: ApplicationConfig = {
providers: [
provideStore({
// Register reducers
reducers: {
product: productReducer,
},
// Add extensions
extensions: [
new ReduxDevtoolsExtension({ name: 'Signal Store Demo' }),
new ImmutableStateExtension(),
],
}),
// Register effects
provideEffects(ProductEffects),
],
};
Boostrap application:
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err),
);
It is possible to register reducers together with a lazy loaded component (see provideFeature
).
For registering effects you can use provideEffects
.
import { Routes } from '@angular/router';
import { ProductShellComponent } from './products-shell/product-shell.component';
import { productReducer } from './state/product.reducer';
import {
provideEffects,
provideFeature,
} from '@mini-rx/signal-store';
import { ProductEffects } from './state/product.effects';
export const productRoutes: Routes = [
{
path: '',
component: ProductShellComponent,
// Lazy load the products state
providers: [
provideFeature('products', productReducer),
provideEffects(ProductEffects),
],
},
];
Memoized selectors are used to select state from the global state object.
You can compose selectors from other selectors, which makes code reuse easy.
Last but not least, memoized selectors can be good for performance (if you have to perform more complex computations for selecting state).
import {
createFeatureStateSelector,
createSelector,
} from '@mini-rx/signal-store';
import { Product } from '../models';
// State interface
export type ProductsState = {
list: Product[];
}
// Memoized selectors
const getProductsFeature = createFeatureStateSelector<ProductsState>('product');
export const getProducts = createSelector(getProductsFeature, (state) => state.list);
FYI Memoized selectors use Signal computed
internally to reduce the amount of calculations.
export class ProductShellComponent implements OnInit {
private store = inject(Store);
products: Signal<Product[]> = this.store.select(getProducts);
}
Your components can read state from the store via the select
method.
select
returns an Angular Signal.
The dispatch
method is used to notify the store about new events (aka actions).
import { Component, inject, OnInit, Signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Store } from '@mini-rx/signal-store';
import { Product } from '../models';
import { getProducts } from '../state';
import {
deleteProduct,
loadProducts,
} from '../state/product.actions';
@Component({
// ...
})
export class ProductShellComponent implements OnInit {
private store = inject(Store);
products: Signal<Product[]> = this.store.select(getProducts);
ngOnInit() {
this.store.dispatch(loadProducts());
}
deleteProduct(todo: Product) {
this.store.dispatch(deleteProduct({ id: todo.id }));
}
}
MiniRx Signal Store offers several extensions out-of-the-box to extend the functionality of the Redux Store. All registered Redux Store extensions are automatically available for all Feature Stores as well.
Extensions are registered for the Redux Store via the provideStore
function (see Store Setup).
FYI You can also write your own extensions!
Inspect global state with the Redux DevTools:
By default, it is possible in Angular to mutate the state of a Signal. This can cause unexpected behaviour and bugs...
With the Immutable State Extension you can enforce Signal state immutability. An error will be thrown if you accidentally mutate state.
Use the Undo Extension to undo dispatched actions. This can be useful to e.g. undo optimistic updates.
console.log the current action and the updated state
-
Less Boilerplate: With the
FeatureStore
API you can update state without writing actions and reducers - A Feature Store manages feature state directly
- The state of a Feature Store integrates into the global state object
- Feature Stores are destroyable
-
setState
update feature state directly with a minimum of boilerplate -
select
read feature state as Angular Signal -
rxEffect
trigger side effects like API calls and handle race-conditions with RxJS flattening operators- effects can be triggered with Signals, Observables and Raw Values
-
connect
connect external sources like Signals or Observables to your feature state -
undo
undo state changes (requires the Undo extension)
A typical Feature Store looks like this (a Singleton Angular service which extends FeatureStore
):
import { inject, Injectable, Signal } from '@angular/core';
import { FeatureStore } from '@mini-rx/signal-store';
import { Todo } from './models';
import { TodoApiService } from './todos-api.service';
type TodoState = {
list: Todo[];
};
const initialState: TodoState = {
list: [],
};
@Injectable({
providedIn: 'root',
})
export class TodosStoreService extends FeatureStore<TodoState> {
private api = inject(TodoApiService);
todosDone: Signal<Todo[]> = this.select((state) =>
state.list.filter((item) => item.isDone),
);
todosNotDone: Signal<Todo[]> = this.select((state) =>
state.list.filter((item) => !item.isDone),
);
constructor() {
super('todo', initialState);
}
loadTodos(): void {
this.api
.getTodos()
.subscribe((todos) => this.setState({ list: todos }));
}
toggleDone(todo: Todo): void {
this.setState((state) => ({
list: state.list.map((item) =>
item.id === todo.id
? { ...item, isDone: !item.isDone }
: item,
),
}));
}
}
You can see that state is read via the select
method. Instead of dispatching actions you use setState
to update state directly with a minimum of boilerplate.
Feature Stores use Redux under the hood and their state becomes part of the global state object.
For that reason you can easily debug your Feature Stores with the Redux DevTools.
FYI Provide a name
parameter to setState
in order to trace the corresponding action in the Redux DevTools:
this.setState({ list: todos }, 'loadTodosSuccess');
When your state becomes more complex, Feature Store will scale with your state management needs.
- Use memoized selectors (
createFeatureStateSelector
,createSelector
) which are great for code reuse and performance- Memoized selectors can easily be moved to another file (which is great if your StoreService grows)
- Use
rxEffect
to trigger side effects like API calls and handle race conditions (e.g. with RxJSswitchMap
)
Following Feature Store uses memoized selectors and effects:
import { inject, Injectable, Signal } from '@angular/core';
import {
createFeatureStateSelector,
createSelector,
FeatureStore,
tapResponse,
} from '@mini-rx/signal-store';
import { Todo } from './models';
import { TodoApiService } from './todos-api.service';
import { switchMap } from 'rxjs';
// Memoized Selectors
const getFeatureState = createFeatureStateSelector<TodoState>();
const getList = createSelector(getFeatureState, (state) => state.list);
const getTodosDone = createSelector(getList, (list) =>
list.filter((item) => item.isDone),
);
const getTodosNotDone = createSelector(getList, (list) =>
list.filter((item) => item.isDone),
);
@Injectable({
providedIn: 'root',
})
export class TodosStoreService extends FeatureStore<TodoState> {
private api = inject(TodoApiService);
todosDone: Signal<Todo[]> = this.select(getTodosDone);
todosNotDone: Signal<Todo[]> = this.select(getTodosNotDone);
// Create an Effect
loadTodos = this.rxEffect<void>(
switchMap(() =>
this.api.getTodos().pipe(
tapResponse({
next: (todos) => this.setState({ list: todos }),
error: (err) => console.log(err),
}),
),
),
);
}
You can easily create Feature Stores which are bound to the component life-cycle.
Simply create a Feature Store inside your component.
This example uses the functional creation method createFeatureStore
which creates a new Feature Store instance for us:
@Component({
// ...
})
export class TodosShellComponent implements OnInit {
private api = inject(TodoApiService);
todoStore = createFeatureStore('todo', initialState);
todosDone: Signal<Todo[]> = this.todoStore.select((state) =>
state.list.filter((item) => item.isDone),
);
todosNotDone: Signal<Todo[]> = this.todoStore.select((state) =>
state.list.filter((item) => !item.isDone),
);
ngOnInit(): void {
this.loadTodos();
}
loadTodos() {
this.api.getTodos().subscribe((todos) => this.todoStore.setState({ list: todos }));
}
}
The "todo" Feature Store will be created and destroyed together with the component.
This works, because Feature Store uses Angular DestroyRef
internally.
In the Redux DevTools you will see that the "todo" Feature Store had been created and destroyed...
Create:
Destroy:
We have just seen, how Feature Stores can be used to manage local component state. But Feature Stores integrate into the global state object and make use of the Redux Store internally.
With Component Stores you can manage state which should not become part of the global state object.
Furthermore, Component Stores can be used as a performance optimization if you have very frequent state updates or many store instances.
Component Store has the same API as Feature Store. Refactoring from Component Store to Feature Store and vice versa means changing two lines of code.
A typical Component Store is created within your component code using the createComponentStore
creation function.
import { Component, inject, OnInit, Signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { createComponentStore } from '@mini-rx/signal-store';
import { Todo } from './models';
import { TodoApiService } from './todos-api.service';
@Component({
// ...
})
export class TodosShellComponent implements OnInit {
private api = inject(TodoApiService);
// Using Component Store!
// We do not need a feature key
todoStore = createComponentStore(initialState);
todosDone: Signal<Todo[]> = this.todoStore.select((state) =>
state.list.filter((item) => item.isDone),
);
todosNotDone: Signal<Todo[]> = this.todoStore.select((state) =>
state.list.filter((item) => !item.isDone),
);
ngOnInit(): void {
this.loadTodos();
}
loadTodos() {
this.api.getTodos().subscribe((todos) => this.todoStore.setState({ list: todos }));
}
}
FYI Do you prefer a dedicated Store service? Use an Injectable service which extends ComponentStore
:
import { Injectable } from '@angular/core';
import { ComponentStore } from '@mini-rx/signal-store';
@Injectable({
providedIn: 'root',
})
export class TodosStoreService extends ComponentStore<TodoState> {
constructor() {
super(initialState);
}
}
You can guess it already... for more complex component states you can use memoized selectors (createComponentStateSelector
, createSelector
) and the rxEffect
method.
Most extensions are compatible with Component Store as well (all except the Redux DevTools extension).
You register extensions globally for all Component Stores with the provideComponentStoreConfig
configuration.
Add provideComponentStoreConfig
to the providers array of the appConfig:
import {
provideComponentStoreConfig,
ImmutableStateExtension,
} from '@mini-rx/signal-store';
export const appConfig: ApplicationConfig = {
providers: [
// ...
provideComponentStoreConfig({
extensions: [new ImmutableStateExtension()]
})
],
};
Alternatively, it is possible to configure extensions individually for each Component Store:
const store = createComponentStore(initialState, {extensions: [new ImmutableStateExtension()]})
In modern Angular, Observables and Signals will coexist... Therefore, modern Angular state management should help you to streamline the usage of Observables and Signals. These MiniRx Signal Store APIs can handle both Observables and Signals:
Available in Feature Store, Component Store.
rxEffect
is used to trigger side effects like API calls.
There are three different ways to trigger the side effect:
- Raw Value
- Signal
- Observable
The example below listens to Signal changes in order to fetch new data.
In Angular 17.1 we have Signal Inputs... E.g. you could use a Signal (Input) to fetch the component data:
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)
}
}
Instead of a Signal, an Observable or a Raw Value could be used to trigger the API call.
Available in Feature Store, Component Store.
With connect
you have the possibility to connect your store with external sources like Observables and Signals.
This helps to make your store the Single Source of Truth for your state.
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 }));
}
}
Do you still use Angular Modules in your Angular applications? We got you covered!
In module-based Apps you can use the classic module APIs:
StoreModule.forRoot()
, StoreModule.forFeature()
, EffectsModule.register()
and ComponentStoreModule.forRoot()
.
MiniRx was successfully tested in these projects:
MiniRx Signal Store for Angular - API Preview
MIT