This is the playground for the Angular Signals version of MiniRx Store.
An official RFC in the MiniRx repo will follow soon.
This playground is used to form the initial ideas for the RFC.
MiniRx is a highly flexible state management solution and scales with your needs:
- 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
MiniRx always tries to find the sweet spot between powerful, simple and lightweight.
Take the great concept of MiniRx Store and create a state management library which embraces Angular Signals.
MiniRx Signal Store will probably be a dedicated library for Angular... It has a lot in common with the original MiniRx library, but the differences are big enough to justify a dedicated library.
The focus on Signals allows (again) to create one of the most lightweight state management libraries for Angular.
Proposed package name: @mini-rx/signal-store
- no lazy state initialisation (Signals encourage to have a meaningful initial state)
- state is only available as Signal
- via the
select
method - via the
state
property - "memoized selectors" (
createSelector
) use Angularcomputed
internally - maybe later a
selectObservable
method could be added for selecting state as Observable
- via the
- all internal usages of RxJS BehaviorSubject have been refactored to Angular Signal
- RxJS effects APIs use
rxEffect
naming to prevent confusion with Angular Signaleffect
API update
instead ofsetState
(inspired by the Angular Signals API)- consider alternatives to
update
? updateState? to be very explicit?
- consider alternatives to
Signals are the new reactive primitive of Angular.
MiniRx Signal Store uses Signals internally, and the state of the Store is exposed as Signal.
Signals in Angular have some advantages, compared to RxJS:
- write subscription free code, even without using the async pipe
- easier to learn (no pipe, no operators, Signals are always synchronous)
- easier to compose derived state from other Signals with
computed
instead of RxJScombineLatest
- potentially more performant Change Detection in the future
MiniRx Signal Store is driven by RxJS, which is great for handling async tasks like API calls (e.g. in the MiniRx effects API).
The Usage of RxJS in the Signal Store is limited:
- The actions stream of the Redux Store is a RxJS Subject
- The effects APIs of the Signal Store use the existing action stream or an own Subject (ComponentStore)
- Only the absolute necessary operators are used (as MiniRx Store did already)
MiniRx Signal Store is based on a straight copy of MiniRx Store and has mostly all the features of the original MiniRx (see the Docs to get an impression: https://mini-rx.io/).
Probably, Signal Store and MiniRx Store will depend on a shared library which will provide common code. All libraries will reside in the same Nx mono-repo.
The original MiniRx Store will still be maintained and stays a perfect state management library if you want to go all-in with RxJS (...and it is framework agnostic!).
Run ng serve
for a dev server. Navigate to http://localhost:4200/
. The application will automatically reload if you change any of the source files.
You should see something like this in the browser:
Or run the MiniRx Demo with the MiniRx Signals Store.
Run ng serve signal-store-demo
You should see this:
The example code is taken from the app.component.ts and app.module.ts in this repo.
You will see:
- Basic usage of the Redux API
- Basic usage of a Feature Store
- Usage of extensions: UndoExtension, Redux DevTools Extension, Immutable Extension
App component:
import {Component, inject} from '@angular/core';
import {createFeatureStateSelector, createSelector, Store,} from '@mini-rx/signal-store';
import {action, on, reducer} from 'ts-action';
import {CounterFeatureStore} from './counter-feature-store';
// Actions
const increment = action('increment');
const decrement = action('decrement');
// Reducer
// The reducer is registered in the App Module
export const counterReducer = reducer(
1,
on(increment, (state) => state + 1),
on(decrement, (state) => state - 1)
);
// Memoized selectors
const getCounterState = createFeatureStateSelector<number>('count');
const getDoubleCount = createSelector(getCounterState, (count) => {
return count * 2
})
@Component({
selector: 'app-root',
template: `
<h3>Store (Redux)</h3>
<p>Counter: {{ count() }}</p>
<p>Counter Double: {{ doubleCount() }}</p>
<button (click)="dec()">Dec</button>
<button (click)="inc()">Inc</button>
<h3>Feature Store</h3>
<p>CounterFs: {{ counterFs.count() }}</p>
<p>CounterFs Double: {{ counterFs.doubleCount() }}</p>
<button (click)="counterFs.dec()">Dec</button>
<button (click)="counterFs.inc()">Inc</button>
<button (click)="counterFs.undoLast()">Undo Last Action</button>
<button (click)="counterFs.mutate()">Mutate</button>
`,
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
// Feature Store
counterFs = inject(CounterFeatureStore);
// Store (Redux)
private store = inject(Store);
count = this.store.select(getCounterState);
doubleCount = this.store.select(getDoubleCount);
inc() {
this.store.dispatch(increment());
}
dec() {
this.store.dispatch(decrement());
}
}
App module:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import {AppComponent, counterReducer} from './app.component';
import {ImmutableStateExtension, ReduxDevtoolsExtension, StoreModule, UndoExtension} from "@mini-rx/signal-store";
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
StoreModule.forRoot({
reducers: {
count: counterReducer,
},
extensions: [
new ReduxDevtoolsExtension({
name: 'Signal Store',
}),
new UndoExtension(),
new ImmutableStateExtension()
],
})
],
bootstrap: [AppComponent]
})
export class AppModule { }
Feature Store offers a simplified API to update and read state. It uses Redux under the hood.
import {Action, FeatureStore} from '@mini-rx/signal-store';
import {computed, Injectable} from "@angular/core";
// State interface
interface CounterState {
count: number;
}
// Initial state
const counterInitialState: CounterState = {
count: 11
};
// Extend FeatureStore and pass the State interface
@Injectable({providedIn: 'root'})
export class CounterFeatureStore extends FeatureStore<CounterState> {
// State
count = computed(() => this.state().count);
doubleCount = computed(() => this.count() * 2);
lastAction: Action | undefined
constructor() {
// Call super with the feature key and the initial state
super('countFs', counterInitialState);
}
// Update state with `update`
inc() {
this.lastAction = this.update(state => ({ count: state.count + 1 }));
}
dec() {
this.lastAction = this.update(state => ({ count: state.count - 1 }));
}
undoLast() {
if (this.lastAction) {
// Undo an action
this.undo(this.lastAction)
}
}
mutate() {
// Try a mutation, but this will throw an error with the Immutable Extension
this.state().count = 123;
}
}