@gernsdorfer/ngrx-lite
v21.1.0
Published
[]() [](https://github.com/gernsdorfer/ngrx-lite/action
Maintainers
Readme
NgRxLite
A small Angular state mangement based on NgRx ComponentStore, with some benefits 😎
Synopsis
The current @ngrx/component-store implementation works with its own isolated store. Unfortunately, there is no connection to the global @ngrx/store or the @ngrx/store-devtools.
This Library connects your @ngrx/component-store with the @ngrx/store to share and debug the @ngrx/actions and store.
Benefits
- 🤝 same API as @ngrx/component-store with optional parameters
- ⏱ fast and easy creation of a dynamic Redux store
- ⏳ optional integrated loading state for effects
- 🤯 debuging of application state across different routes
- ⚒️ Redux DevTools support for NgRxLite ComponentsStore for
patchStatesetStatecreatedLoadingEffects
- 💽 supports session storage and local storage
- 🏘 freedom to decide where the store is located: root, module or in the component scope
- 🔛 share the state changes and actions in the NgRx store
- 📑 store the form data for persistance and debugging
- 👂 create effects for global storage
- ✍️ write the tests is much easier
- 👩💻 checkout the sample app
- ▶️ Play with a Demo
- 📖 read the docs
Install
Yarn
yarn add @ngrx/store @ngrx/effects @ngrx/component-store @ngrx/store-devtools @gernsdorfer/ngrx-liteNPM
npm install @ngrx/store @ngrx/effects @ngrx/component-store @ngrx/store-devtools @gernsdorfer/ngrx-liteUsage
- import the
StoreModulefrom NgRx to the root module
@NgModule({
// ...
imports: [StoreModule.forRoot({})]
// ...- create the store with the same API as @ngrx/component-store
export interface MyState {
counter: number;
}
@Component({
selector: 'my-component',
template: '<button (click)="load(\'test\')">',
})
class MyComponent implements OnDestroy {
// create a componentStore
private store = this.storeFactory.createComponentStore<MyState>({
storeName: 'BASIC_COUNTER',
defaultState: { counter: 0 },
});
// read the state
public counterState$: Observable<MyState> = this.store.state$;
constructor(private storeFactory: StoreFactory) {}
increment(counter: number) {
// patch your state
this.store.patchState({ counter });
}
ngOnDestroy() {
// destory the store
this.store.ngOnDestroy();
}
}That's it 🥳
Features
DevTools support
Install and import ngrx/store-devtools und have all the features from the DevTools for your component store.
It's important to set the monitor property in your StoreDevtoolsOptions, otherwise a state import is not possible.
@NgModule({
imports: [
StoreDevtoolsModule.instrument({
name: 'ngrx-lite-demo',
maxAge: 25,
logOnly: false,
// set the monitor property here
monitor: (state, action) => action,
}),
],
})Let's take a look at Redux DevTools and what happens in the example above.
Store is initialized
After the store is initialized you can find the store in the @ngrx/devtools.

Patch state
After patch state you see this in your Redux DevTools. It's possbile to define an custom action name for your patch/set state.

Router store
Import the RouterStoreModule into your main application to debug your state across all visited URLs. This module
stores related URLs to the current store.
So it's possible to replay your state changes by revisiting the related url.
@NgModule({
//...
imports: [RouterStoreModule]
//...Loading store
Create ComponentLoadingStore to set a Loader State while an Effect is running. You have the same API
as createComponentStore with an extra method loadingEffect.
type State = LoadingStoreState<{ counter: number }, { message: string }>;
@Component({
selector: 'my-app-basic-app',
templateUrl: 'loading-effect.html',
})
export class LoadingEffectComponent implements OnDestroy {
// create your loading store
private store = this.storeFactory.createComponentLoadingStore<State['item'], State['error']>({
storeName: 'LOADING_STORE',
});
// read the state
public counterState$: Observable<State> = this.store.state$;
// define your loadingEffect to change the state
public increment = this.store.loadingEffect('increment', (counter: number = 0) => of(counter + 1));
constructor(private storeFactory: StoreFactory) {}
ngOnDestroy() {
// destory the store
this.counterStore.ngOnDestroy();
}
}Let's take a look at Redux DevTools and what happens in the example above.
Store is initialized
After the store is initialized you can find the store in the @ngrx/devtools.

Loader state isLoading changed
For a running Effect isLoading is true and you can show a spinner in your UI.

Effect successfully executed
After an effect was successfully executed the item key is updated.

Effect unsuccessfully executed
After an effect was unsuccessfully executed the error key contains the error.

Auto-Load and Skip-Pre-Flight
loadingEffect accepts two additional options for declarative one-shot loads on mount and pre-flight skipping:
autoLoad: truetriggers the loader exactly once on the next microtask after the wrapper-store is constructed. Only valid for parameter-free effects (compile-time constraint via conditional types).skipWhen: () => booleanis evaluated before every effect run. When it returnstrue, the dispatch is suppressed — applies toautoLoad, manual calls, and any other trigger.
autoLoad fires on both server and client (SSR-correct), so use skipWhen to suppress the duplicate fetch after hydration:
@Injectable({ providedIn: 'root' })
export class ConfigStore {
private store = inject(StoreFactory).createComponentLoadingStore<Config, ApiError>({
storeName: 'CONFIG',
});
public state = this.store.state;
public load = this.store.loadingEffect('load', () => this.api.getConfig(), {
autoLoad: true,
skipWhen: () => this.transferState.hasRestored('CONFIG'),
});
constructor(
private api: ConfigApi,
private transferState: StoreTransferState,
) {}
}The library itself stays SSR-agnostic. Server-side fetching, hydration, and TransferState integration live in your application code; skipWhen is the hook the library exposes for it.
Reactive Loading
reactiveLoadingEffect binds a Signal<P> source to the loading lifecycle. The container provides the source; the store owns the loading mechanics. Internally it builds on loadingEffect, so action stream, DevTools, and repeatActions behave identically.
Mental model — Owner / Driver vs. Consumer
- Owner / Driver: the one container that calls the connect function (typically a route container). Decides when and how loading happens.
- Consumer: any number of components that
inject()the store and readstate()— read-only.
The library enforces the convention with a single-connect guard: a second parallel-active connect for the same store name logs console.error in development mode (silent in production).
@Injectable({ providedIn: 'root' })
export class ProfessionalListStore {
private store = inject(StoreFactory).createComponentLoadingStore<Professional[], ApiError>({
storeName: 'PROFESSIONAL_LIST',
});
public state = this.store.state;
// returns a function the wrapper-author names freely — convention: connect
public connect = this.store.reactiveLoadingEffect('load', (params: SearchParams) => this.api.search(params), { skipSameActions: true });
constructor(private api: ProfessionalApi) {}
}// Owner / Driver: drives the loading lifecycle
@Component({ ... })
export class SearchPageComponent {
private filter = signal({ ... });
private connected = inject(ProfessionalListStore).connect(this.filter);
}// Consumer: read-only, can be used in many places
@Component({
template: `<div *ngFor="let p of store.state().item">...</div>`,
})
export class ResultListComponent {
protected store = inject(ProfessionalListStore);
}A new source value during a pending loader call cancels the in-flight request automatically (switchMap behavior — not configurable). The DestroyRef of the calling injection context tears down the effect and frees the connect slot when the component unmounts.
For multi-source binding, merge upstream signals with computed() before passing one signal to connect — there is no API knob for it.
Form Store
interface Product {
name: string;
}
@Component({
selector: 'my-app-basic-app',
templateUrl: 'persist-form.html',
})
export class PersistFormComponent implements OnDestroy {
productForm = new FormGroup({
name: new FormControl('', [Validators.required]),
lastName: new FormControl('', [Validators.required]),
});
private store = this.storeFactory.createFormComponentStore<Product>({
storeName: 'PRODUCT_FORM',
plugins: {
storage: 'sessionStoragePlugin',
},
formGroup: this.productForm,
});
}Session/Local Storage
Register Session/Locale storage service
- Register Session/Locale storage in your root module
@NgModule({
// ...
providers: [
{provide: SessionStoragePlugin, useValue: sessionStoragePlugin},
{provide: LocalStoragePlugin, useValue: localStoragePlugin}
]
// ...
})- Create new store with a session storage sync option
class MyClass {
private store = this.storeFactory.createComponentStore<{ counter: number }>({
storeName: 'SESSION_COUNTER',
defaultState: {
counter: 0,
},
plugins: {
storage: 'sessionStoragePlugin',
},
});
}Create Effects
For Using createEffect, please install @ngrx/effects and import EffectsModule.forRoot([]) in your root module
export const resetAction = createAction('reset');
class MyClass {
private store = this.storeFactory.createComponentStore<{ counter: number }>({
storeName: 'SESSION_COUNTER',
defaultState: {
counter: 0,
},
});
myEffect = this.store.createEffect((action) =>
action.pipe(
ofType(resetAction),
tap(() => console.log('do sth.')),
),
);
}Listen on actions
listen on custom actions to execute your business logic
export interface MyState {
counter: number;
}
export const resetAction = createAction('reset');
@Injectable()
export class MyStore implements OnDestroy {
private storeFactory = inject(StoreFactory);
private store = this.storeFactory.createComponentStore<MyState>({
storeName: 'BASIC_COUNTER',
defaultState: { counter: 0 },
});
onReset = this.store.onActions([resetAction]);
}export class AppComponent {
private myStore = inject(MyStore);
resetEffect = this.myStore.onReset(() => console.log('Reset was triggered'));
}Testing
Import storeTestingFactory and write your tests. A minimal example can be
found here
.
TestBed.configureTestingModule({
//...
providers: [storeTestingFactory()],
//..
});