NgRx Decision Guide: Global Store vs ComponentStore for Feature‑Local State
Decide between global @ngrx/store and lightweight @ngrx/component-store by comparing scope, boilerplate, lifecycle, and debugging. This guide shows constraints, a comparison table, trade‑offs, and a validation approach to pick the right store.
13 Oct 2025, 17:06 UTC

Problem and Takeaway
Angular developers often face a split decision: should a feature’s state live in the global NgRx Store or in a lightweight @ngrx/component-store service? The right choice influences boilerplate, debugging, and component lifecycle. The key takeaway is simple – use the global store only for truly shared or long‑lived state; use ComponentStore for UI‑local or feature‑local state that does not need to cross routes or survive navigation.
Decision Context
When you build a feature you usually ask:
- Will other components (maybe far from this one) need to read or react to this state?
- Does the state need to persist after a route change or a page reload?
- Do you want Redux DevTools time‑travel on this state?
- Will you need to dispatch actions from many places or just from one component?
Answers to these questions map directly to the two NgRx patterns.
Compact Comparison Table
| Feature | @ngrx/store | @ngrx/component-store |
|---|---|---|
| Scope | Application‑wide (singleton) | Component‑ or route‑level (service instance) |
| State Persistence | Survives navigation until explicitly reset | Destroyed with component; resets on re‑creation |
| Debugging | Redux DevTools, time‑travel, action log | Not visible in DevTools (unless you dispatch global actions) |
| Boilerplate | Actions, reducers, selectors, effects, module imports | SetState, patchState, updater, effect, no actions |
| Test Surface | Many files, action dispatches, selector queries | Single service class, easier to mock |
| Lifecycle | Manual cleanup of subscriptions if not using async pipe | Automatic cleanup when service is destroyed |
| Cross‑Component Communication | Easy via selectors or global actions | Requires explicit output or global action dispatch |
| Performance | Global store updates may trigger many selectors | Only component subscribers, lower overhead |
| RxJS Integration | Full RxJS ecosystem | Full RxJS ecosystem, plus updater and effect helpers |
| Hybrid Pattern | Possible but messy: mix global and component state | Common: component holds UI state, dispatches global actions when needed |
Trade‑Off Analysis
Shared vs. Local State
Global store shines when a value is read by multiple unrelated components – for example, a user authentication token or a language setting. The single source of truth and the action log make it easier to audit changes.
Boilerplate Cost
Every global store feature requires at least one action file, a reducer, a selector, and optionally an effect. In contrast, a ComponentStore can be a single class with updater and effect methods. For a simple toggle or a form draft, the global store is overkill.
Lifecycle and Memory Leaks
When a component is destroyed, its ComponentStore instance is also destroyed, automatically unsubscribing all internal subjects. The global store never cleans itself up – you must manually clear state if you want to reset it.
Debugging Visibility
Redux DevTools is a powerful feature for time‑travel debugging. If you need to see every state change in the global log, prefer @ngrx/store. ComponentStore state changes are invisible, which can reduce noise but hides diagnostics when you need to trace a bug.
Hybrid Use‑Case
Often the best pattern is a hybrid one: keep UI‑local state in a ComponentStore and dispatch a global action when the state needs to be shared. For example, a search filter that only affects a single list can live in a ComponentStore, but when the filter should also affect another page you dispatch a SetFilter action to the global store.
Concrete Validation Approach
To confirm your decision, implement a small feature twice – once with the global store and once with a ComponentStore – and compare the results. The example below uses a filterable list of products.
Global Store Implementation (Quick Sketch)
// actions.ts
export const setFilter = createAction('[Product] Set Filter', props<{filter: string}>());
// reducer.ts
export const productReducer = createReducer(
initialState,
on(setFilter, (state, {filter}) => ({...state, filter}))
);
// selectors.ts
export const selectFilter = createSelector(selectProductState, state => state.filter);
// component.ts
@Component({selector: 'app-product-list', templateUrl: './list.html', changeDetection: ChangeDetectionStrategy.OnPush})
export class ProductListComponent {
filter$ = this.store.select(selectFilter);
constructor(private store: Store) {}
onFilterChange(value: string) {
this.store.dispatch(setFilter({filter: value}));
}
}
ComponentStore Implementation (Quick Sketch)
// product-store.service.ts
@Injectable()
export class ProductStore extends ComponentStore<{filter: string}> {
constructor() {
super({filter: ''});
}
readonly filter$ = this.select(state => state.filter);
readonly setFilter = this.updater((state, filter: string) => ({...state, filter}));
}
// component.ts
@Component({
selector: 'app-product-list',
templateUrl: './list.html',
providers: [ProductStore],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductListComponent {
filter$ = this.store.filter$;
constructor(private store: ProductStore) {}
onFilterChange(value: string) {
this.store.setFilter(value);
}
}
Validation Steps
- Measure Boilerplate – Count the number of files and lines of code for each implementation. The
ComponentStoreversion should be noticeably smaller. - Test Surface – Write a unit test that dispatches a filter change and asserts the UI updates. The global store test should mock the
Storeand dispatch actions; the component store test can mock the service directly. - Check Lifecycle – In a browser, navigate to the component, type a filter, then navigate away and back. With
ComponentStore, the filter should reset to the default; with the global store, it should persist. - Inspect DevTools – Open Redux DevTools. Only the global store actions should appear. Confirm that the component store’s
setFiltercall does not create an action in the log. - Performance Observation – Use Chrome’s profiler to monitor subscription counts. The component store should have fewer active subscriptions when the component is destroyed.
Practical Checklist for Decision Making
- Is state needed by more than one route or component? Yes → @ngrx/store
- Does state need to survive a route change? Yes → @ngrx/store
- Do you need time‑travel debugging? Yes → @ngrx/store
- Is the state only used within a single component or a tightly coupled feature? No → @ngrx/component-store
- Do you want automatic cleanup on component destroy? Yes → @ngrx/component-store
Final Recommendation
Adopt a hybrid pattern: keep UI‑local state in ComponentStore and use the global store only for truly shared or long‑lived data. This keeps your codebase lean, reduces boilerplate, and leverages the strengths of each NgRx feature.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.