Avoid Manual Store Subscriptions: Prefer Selectors
Consider the following snippet.
@Component({
template: `
<span>{{name}}</span>
`
})
export class ComponentWithStore implements OnInit {
name = '';
constructor(store: Store<AppState>) {}
ngOnInit() {
this.store.subscribe(state => this.name = state.name);
}
}
This approach has several issues. First, a direct subscription introduces the need for manual cleanup (omitted in the example), which adds boilerplate. Second, it forces imperative logic into the component. Most importantly, it fails to leverage the full capabilities of NGRX. A much cleaner solution is:
@Component({
template: `
<span>{{ name$ | async }}</span>
`
})
export class ComponentWithStore {
name$ = this.store.select(state => state.name);
constructor(store: Store<AppState>) {}
}
This version is superior for multiple reasons. We eliminate the unsubscribe boilerplate entirely, as the async pipe handles that automatically. It also enables using ChangeDetectionStrategy.OnPush for improved performance, reduces the overall code footprint, and the component becomes more declarative. Another common scenario:
@Component({
template: `
<span>{{name}}</span>
`
})
export class ComponentWithStore implements OnInit {
name = '';
constructor(store: Store<AppState>) {}
ngOnInit() {
this.store.subscribe(state => {
if (state.name === 'ReservedName') {
this.store.dispatch(reservedNameEncountered());
}
});
}
}
This code checks a specific state value and then dispatches an action. While this pattern may appear in many applications, it is incorrect. Derived state consumers should not dispatch actions in response to state changes; that logic belongs in Effects:
export class Effects {
reservedName$ = createEffect(() => this.actions$.pipe(
ofType(actions.setName),
filter(({payload}) => payload === 'ReservedName'),
map(() => reservedNameEncountered())
));
constructor(actions$: Actions) {}
}
This leaves the component completely free of such logic. Remember, the only way to modify state is through actions, and the reactions to those actions should be handled strictly by Effects and Reducers, not by observing derived state.
So, why the caveat of "almost" never subscribe? Consider a scenario where an application has a dynamic permission system managed by an admin. These permissions, stored in the AppState, can change in real time while a user is interacting with a Reactive form. Certain form fields may need to become disabled or enabled based on these real-time permission changes. How can this be handled?
@Component({
template: `
omitted for brevity
`
})
export class ComponentWithStore implements OnInit {
permissions$ = this.store.select(state => state.permissions);
form = this.formBuilder.group({
firstName: ['', Validators.require],
});
constructor(store: Store<AppState>) {}
ngOnInit() {
this.permissions$.subscribe(permissions => {
const control = this.form.get('firstName');
if (permissions.canEditFirstName) {
control.enable();
} else {
control.disable();
}
});
}
}
The challenge is that a Reactive FormControl can only be disabled through its imperative disable method; there is no declarative alternative. If we must use Reactive forms, and the control's disabled state depends on the AppState, we are forced into a subscription. A practical rule is:
Only subscribe to the Store manually when you must call an imperative third-party function for which no alternative exists, such as
FormControl.disable. And always remember to unsubscribe!
A more modern, subscription-free way to handle this is with @ngrx/component-store and its component effects.
Do Not Pipe Store Observables: Use Named Selectors
Now that subscriptions are avoided, everything is perfect, right? Not quite. See this example:
@Component({
template: `
<span *ngFor="let user of (activeUsers$ | async)">{{ user.name }}</span>
`
})
export class ComponentWithStore {
activeUsers$ = this.store.select(state => state.users).pipe(
map(users => users.filter(user => user.isActive)),
);
constructor(store: Store<AppState>) {}
}
Here, the original state is an Array in the AppState, but the component needs a different list containing only active users. The solution was to apply RxJS operators directly to the derived state's Observable. Is this wrong?
The problem is that even basic RxJS operators are more verbose and complex than a simple selector function. This adds noise and makes debugging complex logic harder. A better approach is to define a named selector:
// selectors.ts
const activeUsers = (state: AppState) => state.users.filter(user => user.isActive)
Then, the selector can be used directly to get the derived state:
@Component({
template: `
<span *ngFor="let user of (activeUsers$ | async)">{{ user.name }}</span>
`
})
export class ComponentWithStore {
activeUsers$ = this.store.select(activeUsers);
constructor(store: Store<AppState>) {}
}
This results in less code, higher readability, and immediately makes it clear what data is being selected, which is more declarative. The solution to this bad practice is to use NGRX selectors.
Avoid piping operators on derived state; use named selectors instead.
Prefer Named Selectors over combineLatest
Application state can be complex, and a derived state often depends on multiple parts of the store. For instance, imagine we maintain an Array of Clothing objects and a separate shopping cart, also an Array of Clothing objects. A user can add items to the cart, and the UI must show an "Add to cart" button or a "Remove from Cart" button depending on whether the item is already in the cart. To achieve this, we need to add a boolean property, e.g., isInShoppingCart, to each item in our derived list, checking if its id is in the cart. We can start with two selectors for the items and the cart, resulting in this component logic:
@Component({
template: `
<app-clothing-item
*ngFor="let item of (clothingItems$ | async)" [item]="item">
</app-clothing-item>
`
})
export class ClothingItemListComponent {
clothingItems$ = combineLatest([
this.store.select(state => state.clothingItems),
this.store.select(state => state.cart),
]).pipe(
map(([items, cart]) => items.map(item => ({
...item,
isInShoppingCart: cart.map(cartItem => cartItem.id).includes(item.id),
})))
);
constructor(store: Store<AppState>) {}
}
It's clear this logic is too heavy for a component class. While declarative, its intent is not obvious until you deeply read the code. The solution, again, is selectors. NGRX lets you combine selectors with the createSelector function:
const allItems = (state: AppState) => state.clothingItems;
const shoppingCart = (state: AppState) => state.shoppingCart;
const cartIds = createSelector(shoppingCart, cart => cart.map(item => item.id));
const clothingItems = createSelector(
allItems,
cartIds,
(items, cart) => items.map(item => ({
...item,
isInShoppingCart: cart.includes(item.id),
}),
);
This refactoring makes the functions much clearer. First, we select the items and the cart. Next, we create a selector that picks out just the cart's ids (selectors are memoized). Finally, we combine the two to transform the list of all items. The component only uses the final selector. You may wonder why we need four selectors. Having many simple, composable selectors is preferable to a few complex ones for reusability and maintainability.
When you see
combineLatestused with derived state, consider combining selectors withcreateSelector.
Avoid Manipulating Nested State with withLatestFrom
Some Effects require reading other parts of the existing store state. For example, imagine a table with client-side sorting and filtering controlled by buttons. We have a setSorting action that takes a sorting object ({field: string, direction: -1 | 1}). This action adds the sorting info to a larger query object, which also contains filters and pagination. After updating the query, the full object is sent to the backend. The backend only accepts the whole query object, but our action only changes the sorting part (nested state). The component that dispatches setSorting might not have the full query, which might lead to this pattern in an effect:
@Injectable()
export class Effects {
getData$ = createEffect(() => this.actions$.pipe(
ofType(setSorting, setFilters, setPagination),
withLatestFrom(this.store.select(state => state.query)),
map(([{payload}, query]) => ({...query, [payload.type]: payload.data})),
exhaustMap(query => this.dataService.getData(query).pipe(
map(response => getDataSuccess(response)),
catchError(error => of(getDataError(error)))
)),
));
constructor(
private readonly actions$: Actions,
private readonly store: Store<AppState>,
private readonly dataService: DataService,
) {}
}
This creates several issues. We now have to handle three separate actions that all trigger the same request (sorting, filtering, and pagination), and we’re using withLatestFrom to pull in state. A better approach is to simplify. First, remove those three actions (setSorting, setFilters and setPagination). As Mike Ryan states in this talk, keeping actions concise is critical. We replace them with a single action, getData, that accepts the entire query object as its payload. The component can now dispatch it like this:
@Component({
template: `
<ng-container *ngIf="query$ | async as query">
<app-sorting (sort)="setSorting($event, query)"></app-sorting>
<app-filters (filter)="setFilters($event, query)"></app-filters>
<app-table-data [data]="data$ | async"></app-table-data>
<app-pagination (paginate)="setPagination($event, query)"></app-pagination>
</ng-container>
`,
})
export class TablePresenterComponent {
query$ = this.store.select(state => state.query);
data$ = this.store.select(state => state.data);
constructor(
private readonly store: Store<AppState>,
) {}
setSorting(sorting: Sorting, query: Query) {
this.store.dispatch(getData({...query, sorting}));
}
setFilters(sorting: Sorting, filters: Filters) {
this.store.dispatch(getData({...query, filters}));
}
setPagination(sorting: Sorting, pagination: Pagination) {
this.store.dispatch(getData({...query, pagination}));
}
}
Notice how the component logic remains simple; each method dispatches the same action for a different scenario. Because the action is now unified, the effect simplifies to:
@Injectable()
export class Effects {
getTableData$ = createEffect(() => this.actions$.pipe(
ofType(getData),
exhaustMap(({payload}) => this.dataService.getData(payload).pipe(
map(response => getDataSuccess(response)),
catchError(error => of(getDataError(error)))
)),
));
constructor(
private readonly actions$: Actions,
private readonly dataService: DataService,
) {}
}
This now handles only one action, performs one simple task, and has eliminated the need for withLatestFrom.
The presence of
withLatestFrominside an effect is often a code smell, indicating that a more straightforward design might be possible.
Never Store Derived State in the Store
A fundamental aspect of using NGRX is distinguishing between original and derived state. Original state is the source data we store in the AppState, such as data received from a backend. Derived state is created from the original state through a transformation, typically a selector. The list of Clothing items with the isInShoppingCart flag from the earlier example is a perfect example of derived state.
It can be tempting to store derived state alongside original state, but this is harmful for several reasons:
- It stores more data than necessary in the store.
- It requires syncing two states; any action modifying the original state now also needs to update the derived state.
- It clutters the
AppStatestructure.
Consider this example:
const _reducer = createReducer(
initialState,
on(clothingActions.filter, (state, {payload}) => ({
...state,
filteredClothings: state.clothings.filter(
clothing => clothing.name.includes(query),
),
})),
);
Here, we have both the original list of clothings and a filtered list. It would be much better to store only the query and derive the filtered list with a selector that combines the query and the full list:
// reducer.ts
const _reducer = createReducer(
initialState,
on(clothingActions.filter, (state, {payload}) => ({
...state,
query: payload,
})),
);
// selectors.ts
const allClothings = (state: AppState) => state.clothings;
const query = (state: AppState) => state.query;
const filteredClothings = createSelector(
allClothings,
query,
(clothings, query) => clothing.filter(
clothing => clothing.name.includes(query),
),
);
You can then directly use the derived state selector in your component.
Conclusion
Notice a recurring theme in the solutions to these bad practices: the use of selectors. The core challenge with any state management system is correctly distinguishing between original and derived state. Making that distinction clear, and using concise selectors to manipulate and retrieve that state, leads to a more simple, declarative, and reactive Angular application with NGRX.
