Skip to main content

Angular NgRx Selectors

Introduction

When working with NgRx in Angular applications, selectors are one of the most powerful features you'll use. Selectors are pure functions that take slices of state from the store and return derived, transformed, or filtered data. Think of selectors as queries for your state - they help components efficiently access exactly the data they need.

In this guide, you'll learn:

  • What NgRx selectors are and why they're essential
  • How to create basic and complex selectors
  • How selectors provide memoization for performance
  • Practical examples of selectors in real-world applications

Understanding NgRx Selectors

What are Selectors?

Selectors are pure functions used for selecting, deriving, and composing pieces of state. They follow a few key principles:

  1. Pure functions - Given the same input, they always return the same output
  2. Efficient - Selectors use memoization to prevent unnecessary recalculations
  3. Composable - You can build complex selectors by combining simpler ones
  4. Decoupled - They separate the knowledge of state structure from components

Why Use Selectors?

Without selectors, components would need to know the exact structure of your store, creating tight coupling. Changes to your state structure would require changes to every component that accesses that state.

Selectors solve this problem by:

  • Acting as an abstraction layer between your state and components
  • Enabling reuse of common data selection logic
  • Improving performance through memoization
  • Simplifying testing

Creating Basic Selectors

Let's start with basic selectors using the createSelector function from NgRx.

First, we'll define a simple state interface:

typescript
// book.model.ts
export interface Book {
id: string;
title: string;
author: string;
year: number;
}

// book.state.ts
export interface BookState {
books: Book[];
selectedBookId: string | null;
loading: boolean;
error: string | null;
}

export interface AppState {
books: BookState;
// other state slices...
}

Now, let's create some basic selectors:

typescript
// book.selectors.ts
import { createSelector, createFeatureSelector } from '@ngrx/store';
import { AppState, BookState } from './book.state';
import { Book } from './book.model';

// Feature selector - selects the books slice of state
export const selectBooksState = createFeatureSelector<AppState, BookState>('books');

// Basic selectors
export const selectAllBooks = createSelector(
selectBooksState,
(state: BookState) => state.books
);

export const selectBooksLoading = createSelector(
selectBooksState,
(state: BookState) => state.loading
);

export const selectBooksError = createSelector(
selectBooksState,
(state: BookState) => state.error
);

export const selectSelectedBookId = createSelector(
selectBooksState,
(state: BookState) => state.selectedBookId
);

Using Selectors in Components

To use selectors in your components, you'll inject the Store and use the select method:

typescript
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { Book } from './book.model';
import { AppState } from './book.state';
import { selectAllBooks, selectBooksLoading } from './book.selectors';

@Component({
selector: 'app-book-list',
template: `
<div *ngIf="loading$ | async">Loading...</div>
<ul *ngIf="!(loading$ | async)">
<li *ngFor="let book of books$ | async">
{{ book.title }} by {{ book.author }} ({{ book.year }})
</li>
</ul>
`
})
export class BookListComponent implements OnInit {
books$: Observable<Book[]>;
loading$: Observable<boolean>;

constructor(private store: Store<AppState>) {}

ngOnInit() {
this.books$ = this.store.select(selectAllBooks);
this.loading$ = this.store.select(selectBooksLoading);
}
}

Creating Complex Selectors

The real power of selectors comes from their composability. You can create complex selectors by combining simpler ones.

Selecting Derived Data

Let's create a selector that selects the currently selected book:

typescript
export const selectSelectedBook = createSelector(
selectAllBooks,
selectSelectedBookId,
(books, selectedId) => {
if (selectedId === null) return null;
return books.find(book => book.id === selectedId) || null;
}
);

Parameterized Selectors

Sometimes you need to create selectors that take parameters. Let's create a selector to find books by a specific author:

typescript
export const selectBooksByAuthor = (authorName: string) => createSelector(
selectAllBooks,
(books) => books.filter(book => book.author === authorName)
);

To use this in a component:

typescript
// In your component
const jkRowlingBooks$ = this.store.select(selectBooksByAuthor('J.K. Rowling'));

Selector with Transformation

Selectors can transform data before returning it:

typescript
export const selectBookTitles = createSelector(
selectAllBooks,
(books) => books.map(book => book.title)
);

export const selectBooksByYear = createSelector(
selectAllBooks,
(books) => {
const booksByYear: { [year: number]: Book[] } = {};
books.forEach(book => {
if (!booksByYear[book.year]) {
booksByYear[book.year] = [];
}
booksByYear[book.year].push(book);
});
return booksByYear;
}
);

Memoization and Performance

One of the key benefits of selectors is memoization - caching the results of expensive calculations to improve performance.

NgRx selectors automatically implement memoization. A selector will recompute its result only when one of its input selectors produces a new value. Otherwise, it returns the cached value from the last computation.

Consider this example:

typescript
// This only re-runs when the books array changes
export const selectBookCount = createSelector(
selectAllBooks,
(books) => books.length
);

// This only re-runs when either the books array or the selectedBookId changes
export const selectSelectedBookTitle = createSelector(
selectSelectedBook,
(book) => book ? book.title : 'No book selected'
);

Advanced Selector Patterns

Entity Adapter Selectors

When using NgRx Entity to manage collections, you can leverage its predefined selectors:

typescript
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import { Book } from './book.model';

export interface BookEntityState extends EntityState<Book> {
selectedBookId: string | null;
loading: boolean;
error: string | null;
}

export const bookAdapter: EntityAdapter<Book> = createEntityAdapter<Book>({
selectId: (book: Book) => book.id,
sortComparer: (a: Book, b: Book) => a.title.localeCompare(b.title),
});

// Initial state
export const initialState: BookEntityState = bookAdapter.getInitialState({
selectedBookId: null,
loading: false,
error: null
});

// Feature selector
export const selectBookState = createFeatureSelector<BookEntityState>('books');

// Entity adapter selectors
export const {
selectIds,
selectEntities,
selectAll,
selectTotal,
} = bookAdapter.getSelectors(selectBookState);

// Now you can use these selectors
export const selectAllBooks = selectAll;
export const selectBookCount = selectTotal;

Using createSelectorFactory

For advanced use cases, you can create custom selector factories with specific memoization logic:

typescript
import { createSelectorFactory, defaultMemoize } from '@ngrx/store';

export const selectTopRatedBooks = createSelectorFactory(
(projectionFn) => defaultMemoize(
projectionFn,
{ someCustomConfiguration: true }
)
)(
selectAllBooks,
(books) => books.filter(book => book.rating > 4.5)
);

Real-World Example: Book Filtering Application

Let's build a more complete example of a book filtering application:

typescript
// State interface
export interface BookState {
books: Book[];
selectedBookId: string | null;
loading: boolean;
error: string | null;
filters: {
searchTerm: string;
yearRange: [number, number];
selectedAuthor: string | null;
};
}

// Selectors
export const selectBooksState = createFeatureSelector<AppState, BookState>('books');

export const selectAllBooks = createSelector(
selectBooksState,
state => state.books
);

export const selectFilters = createSelector(
selectBooksState,
state => state.filters
);

export const selectSearchTerm = createSelector(
selectFilters,
filters => filters.searchTerm
);

export const selectYearRange = createSelector(
selectFilters,
filters => filters.yearRange
);

export const selectSelectedAuthor = createSelector(
selectFilters,
filters => filters.selectedAuthor
);

export const selectFilteredBooks = createSelector(
selectAllBooks,
selectSearchTerm,
selectYearRange,
selectSelectedAuthor,
(books, searchTerm, yearRange, selectedAuthor) => {
return books.filter(book => {
// Filter by search term
const matchesSearch = searchTerm ?
book.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
book.author.toLowerCase().includes(searchTerm.toLowerCase()) :
true;

// Filter by year range
const matchesYearRange = book.year >= yearRange[0] && book.year <= yearRange[1];

// Filter by selected author
const matchesAuthor = selectedAuthor ? book.author === selectedAuthor : true;

return matchesSearch && matchesYearRange && matchesAuthor;
});
}
);

export const selectAuthors = createSelector(
selectAllBooks,
books => {
const authors = books.map(book => book.author);
return [...new Set(authors)].sort(); // Get unique authors and sort alphabetically
}
);

export const selectYearBounds = createSelector(
selectAllBooks,
books => {
if (books.length === 0) return [1900, 2023];
const years = books.map(book => book.year);
return [Math.min(...years), Math.max(...years)];
}
);

Now let's implement a component that uses these selectors:

typescript
@Component({
selector: 'app-book-filter',
template: `
<div class="filters">
<input
type="text"
placeholder="Search books..."
[ngModel]="searchTerm$ | async"
(ngModelChange)="updateSearchTerm($event)">

<select
[ngModel]="selectedAuthor$ | async"
(ngModelChange)="updateSelectedAuthor($event)">
<option [ngValue]="null">All Authors</option>
<option *ngFor="let author of authors$ | async" [ngValue]="author">
{{ author }}
</option>
</select>

<div class="year-range" *ngIf="yearBounds$ | async as bounds">
<label>Year Range: {{ yearRange$ | async | json }}</label>
<input
type="range"
[min]="bounds[0]"
[max]="bounds[1]"
[ngModel]="(yearRange$ | async)[0]"
(ngModelChange)="updateYearMin($event)">
<input
type="range"
[min]="bounds[0]"
[max]="bounds[1]"
[ngModel]="(yearRange$ | async)[1]"
(ngModelChange)="updateYearMax($event)">
</div>
</div>

<div class="book-list">
<div *ngIf="loading$ | async">Loading books...</div>
<div *ngIf="error$ | async as error" class="error">{{ error }}</div>

<div *ngIf="(filteredBooks$ | async)?.length === 0" class="no-results">
No books match your filters
</div>

<app-book-card
*ngFor="let book of filteredBooks$ | async"
[book]="book"
[selected]="(selectedBookId$ | async) === book.id"
(select)="selectBook(book.id)">
</app-book-card>
</div>
`
})
export class BookFilterComponent implements OnInit {
filteredBooks$: Observable<Book[]>;
loading$: Observable<boolean>;
error$: Observable<string>;

authors$: Observable<string[]>;
yearBounds$: Observable<[number, number]>;

searchTerm$: Observable<string>;
selectedAuthor$: Observable<string | null>;
yearRange$: Observable<[number, number]>;
selectedBookId$: Observable<string | null>;

constructor(private store: Store<AppState>) {}

ngOnInit() {
this.filteredBooks$ = this.store.select(selectFilteredBooks);
this.loading$ = this.store.select(selectBooksLoading);
this.error$ = this.store.select(selectBooksError);

this.authors$ = this.store.select(selectAuthors);
this.yearBounds$ = this.store.select(selectYearBounds);

this.searchTerm$ = this.store.select(selectSearchTerm);
this.selectedAuthor$ = this.store.select(selectSelectedAuthor);
this.yearRange$ = this.store.select(selectYearRange);
this.selectedBookId$ = this.store.select(selectSelectedBookId);
}

// Methods to dispatch actions based on user interaction
updateSearchTerm(term: string) {
this.store.dispatch(BookActions.updateSearchTerm({ searchTerm: term }));
}

updateSelectedAuthor(author: string | null) {
this.store.dispatch(BookActions.updateSelectedAuthor({ selectedAuthor: author }));
}

updateYearMin(min: number) {
this.store.select(selectYearRange).pipe(
take(1)
).subscribe(([_, max]) => {
this.store.dispatch(BookActions.updateYearRange({ yearRange: [min, max] }));
});
}

updateYearMax(max: number) {
this.store.select(selectYearRange).pipe(
take(1)
).subscribe(([min, _]) => {
this.store.dispatch(BookActions.updateYearRange({ yearRange: [min, max] }));
});
}

selectBook(bookId: string) {
this.store.dispatch(BookActions.selectBook({ bookId }));
}
}

Common Patterns and Best Practices

Selector Organization

  1. Feature-based organization: Keep selectors close to the feature state they select from
  2. Export all selectors from a barrel file: Makes imports cleaner
  3. Group related selectors: Keep related selectors together for easier maintenance

Naming Conventions

  • Prefix with select to make it clear it's a selector
  • Name should clearly describe what data it returns
  • Be consistent across your application

Performance Considerations

  • Avoid creating selectors inside components (they won't be memoized properly)
  • Be mindful of expensive operations in selectors
  • For very complex data transformations, consider using the @ngrx/component-store library

Testing Selectors

Selectors are pure functions, making them easy to test:

typescript
// book.selectors.spec.ts
describe('Book Selectors', () => {
const initialState: BookState = {
books: [
{ id: '1', title: 'Book 1', author: 'Author A', year: 2020 },
{ id: '2', title: 'Book 2', author: 'Author B', year: 2021 },
],
selectedBookId: '1',
loading: false,
error: null,
filters: {
searchTerm: '',
yearRange: [2000, 2023],
selectedAuthor: null
}
};

it('should select all books', () => {
const result = selectAllBooks.projector(initialState);
expect(result.length).toBe(2);
expect(result[0].title).toBe('Book 1');
});

it('should select filtered books based on search term', () => {
const books = [
{ id: '1', title: 'Angular Book', author: 'Author A', year: 2020 },
{ id: '2', title: 'React Book', author: 'Author B', year: 2021 },
];
const searchTerm = 'Angular';
const yearRange = [2000, 2023];
const selectedAuthor = null;

const result = selectFilteredBooks.projector(
books, searchTerm, yearRange, selectedAuthor
);

expect(result.length).toBe(1);
expect(result[0].title).toBe('Angular Book');
});
});

Summary

NgRx selectors are a powerful tool for efficiently querying and transforming state in your Angular applications. They provide:

  • Memoization: Preventing unnecessary recalculations and improving performance
  • Composability: Building complex queries from simpler ones
  • Abstraction: Hiding state structure details from components
  • Reusability: Writing selection logic once and using it throughout your app
  • Testability: Pure functions that are easy to test

By using selectors effectively, you can build more maintainable and performant Angular applications.

Further Resources

Exercises

  1. Create a selector that returns the number of books published in each decade (1990s, 2000s, 2010s, etc.)
  2. Implement a selector that returns a boolean indicating if there are any books in the store that match the current filters
  3. Create a parameterized selector that returns books rated higher than a given rating value
  4. Extend the book filtering example to include filtering by genre (assume books have a genre property)
  5. Implement a selector that returns the average publication year of the filtered books


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)