Choosing Between NgRx Store and ComponentStore for Angular State Management
A decision guide that outlines when to use the global NgRx Store versus the scoped ComponentStore, compares trade‑offs, and shows a concrete ComponentStore implementation with validation steps.
28 Sept 2026, 21:20 UTC

Decision: When to Use NgRx Store vs ComponentStore
Start by defining the scope of the state you need to manage. If the state must be read or updated by multiple unrelated features, services, or lazy‑loaded modules, the global @ngrx/store Store is the appropriate choice. If the state is owned by a single feature, component, or a tightly‑coupled component tree and does not require cross‑module DevTools time‑travel, @ngrx/component-store ComponentStore reduces boilerplate and keeps the state scoped.
Constraints to Consider
- Shared access: Does more than one independent part of the application need the same state?
- DevTools visibility: Is Redux DevTools time‑travel debugging required for this state?
- Boilerplate tolerance: Are you willing to write actions, reducers, selectors, and effects for minimal UI state?
- Lifecycle: Should the state survive beyond the component that created it?
Comparison Table
| Aspect | NgRx Store | NgRx ComponentStore |
|---|---|---|
| Scope | Application‑wide (singleton) | Component or service scoped |
| Boilerplate | Actions, reducers, selectors, effects (optional) | Updaters and effects only; selectors derived from state observable |
| DevTools Integration | Full Redux DevTools support (time‑travel, action logging) | No automatic DevTools entry; must expose selectors to Store for visibility |
| State Lifetime | Lives for the app lifetime until reset | Destroyed with the component/service unless retained manually |
| Installation Size | ~10 KB gzipped (@ngrx/store + @ngrx/effects) | ~4 KB gzipped (@ngrx/component-store) |
Trade‑Off Explanation
Using the global Store gives you a single source of truth and powerful debugging, but it forces every consumer to subscribe to a broad observable, potentially causing unnecessary change detection cycles. ComponentStore isolates state, so only the owning component (or its providers) receives updates, reducing the risk of stray subscriptions. However, if you later discover that another feature needs the same state, you must refactor to a shared Store or lift the ComponentStore to a shared service, which adds migration overhead.
Concrete Implementation: Feature‑Local UI State with ComponentStore
Consider a feature that displays a list of items and allows the user to toggle a “show completed” filter. The filter state is local to the feature and does not need to be shared.
1. Define the ComponentStore
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
interface ItemsFilterState {
showCompleted: boolean;
}
@Injectable()
export class ItemsFilterStore extends ComponentStore<ItemsFilterState> {
// initial state
constructor() {
super({ showCompleted: false });
}
// readonly observable for the template
readonly showCompleted$ = this.select(state => state.showCompleted);
// updater to toggle the flag
readonly toggleShowCompleted = this.updater((state) => ({
...state,
showCompleted: !state.showCompleted
}));
// example effect that could fetch data based on the flag
readonly loadItems$ = this.effect((trigger: Observable<unknown>) =>
trigger.pipe(
tapResponse(
() => {
// placeholder for actual service call
console.log('Loading items with showCompleted =', this.get().showCompleted);
},
(err) => console.error(err)
)
));
}
2. Use the Store in a Component
import { Component } from '@angular/core';
import { ItemsFilterStore } from './items-filter.store';
import { Observable } from 'rxjs';
@Component({
selector: 'app-items-filter',
template: `
Show completed
`,
providers: [ItemsFilterStore] // scoped to this component
})
export class ItemsFilterComponent {
readonly showCompleted$: Observable<boolean>;
constructor(private filterStore: ItemsFilterStore) {
this.showCompleted$ = this.filterStore.showCompleted$;
}
toggle() {
this.filterStore.toggleShowCompleted();
// optional: trigger a data reload
this.filterStore.loadItems$();
}
}
3. Validation – Runtime Check
To confirm that the state is truly scoped, open the browser dev tools, navigate to the Angular > Profiler tab (or use ng.probe if enabled), locate the component instance, and verify that its ItemsFilterStore property exists. Changing the checkbox should update the UI without affecting other feature components that do not provide the same store.
4. Validation – Unit Test
A simple Jasmine test asserts that the updater modifies the observable correctly and that the store is destroyed when the component is.
import { TestBed } from '@angular/core/testing';
import { ItemsFilterStore } from './items-filter.store';
describe('ItemsFilterStore', () => {
let store: ItemsFilterStore;
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ItemsFilterStore] });
store = TestBed.inject(ItemsFilterStore);
})
it('should toggle showCompleted', (done) => {
store.showCompleted$.subscribe((value) => {
expect(value).toBeFalse(); // initial
store.toggleShowCompleted();
});
store.showCompleted$.subscribe((value) => {
expect(value).toBeTrue();
done();
});
})
it('should be destroyed with its provider', () => {
// In a real test you would create a host component, destroy it,
// and ensure no memory leak; here we just show the concept.
expect(store).toBeDefined();
})
});
Limitations and How to Check the Result
- ComponentStore does not automatically appear in Redux DevTools. If you need time‑travel debugging for this local state, expose a selector to a temporary NgRx Store slice or use the
devtoolsoption from@ngrx/store-devtoolsmanually. - Sharing a ComponentStore instance across unrelated components requires providing it at a shared service level; otherwise each component gets its own copy, which may lead to inconsistent UI.
- NgRx APIs evolve; verify that the version of
@ngrx/component-storeyou use matches the patterns shown (class‑based updaters/effects).
To check that your decision matches operational needs, build two small prototypes of the same feature: one using the global Store (with actions/reducers) and one using ComponentStore as shown. Compare bundle size (via source-map-explorer), count of boilerplate files, and DevTools visibility. Choose the option that satisfies your shared‑access and debugging requirements while minimizing unnecessary complexity.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.