LogoLogo
v3.7
v3.7
  • README
  • Getting Started
    • Why
    • Installation
  • Concepts
    • Introduction
    • Store
    • Actions
    • State
    • Select
  • Advanced
    • Action Handlers
    • Actions Life Cycle
    • Cancellation
    • Composition
    • Error Handling
    • Ivy Migration Guide
    • Lazy Loading
    • Life-cycle
    • Mapped Sub States
    • Meta Reducers
    • Optimizing Selectors
    • Options
    • Shared State
    • State Token
    • State Operators
    • Sub States
  • Recipes
    • Authentication
    • Caching
    • Component Events from NGXS
    • Debouncing Actions
    • Dynamic Plugins
    • Immutability Helpers
    • Module Federation
    • Style Guide
    • Unit Testing
    • RxAngular Integration
  • Snippets
    • State Operators
  • Plugins
    • Introduction
    • CLI
    • Logger
    • Devtools
    • Storage
    • Forms
    • Web Socket
    • Router
    • HMR
  • NGXS Labs
    • Introduction
  • Community
    • FAQ
    • Resources
    • Contributors
    • Contributing
    • Sponsors
  • Changelog
Powered by GitBook
On this page
  • Memoization
  • Implementation
  1. Advanced

Optimizing Selectors

PreviousMeta ReducersNextOptions

Last updated 2 years ago

are responsible for providing state data to your application. As your application code grows, naturally the number of selectors you create also increases. Ensuring your selectors are optimized can be instrumental in building a faster performing application.

Memoization

Selectors are memoized functions. Memoized functions are calculated when their arguments change and the results are cached. Regardless of how many components or services consume a selector, a selector will calculate only once when state changes and the cached result will be returned to all consumers. Taking advantage of this feature can result in performance increases.

For example, there exists this state model:

interface SomeStateModel {
  data: Data[];
  name: string;
}

And in this example there is an input component where a user can type a name. On key down, an action is dispatched updating the name property of state. On the same page, another component renders data. In order to render state data we create a selector in our state class:

@Selector()
static getViewData(state: SomeStateModel) {
   return state.data.map(d => expensiveFunction(d));
}

Selectors defined in state classes implicitly have state injected as their first argument. The above selector will be recalculated every time the user types into the input component. Since state could update rapidly when a user types, the expensive selector will needlessly recalculate even though it does not care about the name property of state changing. This selector does not take advantage of memoization.

One way to solve this problem is to turn off the injectContainerState selector at root, state, or selector level. By default (in NGXS v3), the state is implicitly injected as the first argument for composite selectors defined within state classes. Turning off this setting prevents the container state from being injected as the first argument. This requires you to explicitly specify all arguments when you use the @Selector([...]) decorator. Any parameterless @Selector() decorators will still inject the state as an implicit argument. Note that this option does not apply to selectors declared outside of state classes (because there is no container state to inject). For example, we create two selectors in our state class:

@Selector([SomeState])
static getData(state: SomeStateModel) {
   return state.data;
}

@Selector([SomeState.getData])
static getViewData(data: Data[]) {
  return data.map(d => expensiveFunction(d));
}

This getViewData selector will not be recalculated when a user types into the input component. This selector targets the specific property of state it cares about as its argument by leveraging an additional selector. When the name property of state changes, the getViewData arguments do not change. Memoization is taken advantage of.

@State({...})
@Injectable()
export class SomeState {
  @Selector()
  static getData(state: SomeStateModel) {
    return state.data;
  }
}

export class SomeStateQueries {
  @Selector([SomeState.getData])
  static getViewData(data: Data[]) {
    return data.map(d => expensiveFunction(d));
  }
}

Implementation

Selectors are calculated when state changes. As your application grows, the number of state changes increases. Finding optimizations in your selector implementations can have significant benefits.

For example, say you have this state model:

interface SelectedDataStateModel {
  selectedIds: number[];
}

And you have this selector:

@Selector([SelectedDataState])
isDataSelected(state: SelectedDataStateModel) {
  return (id: number) => state.selectedIds.includes(id);
}
<ng-container *ngIf="isDataSelected$ | async as isDataSelected">
  <data-check-box
    *ngFor="data of data$ | async"
    [checked]="isDataSelected(d.id)"
  ></data-check-box>
</ng-container>

When a user checks or unchecks an item, state.selectedIds is updated, therefore the isDataSelected selector is recalculated and the list must re-render. Every time the list re-renders, the lazy selector isDataSelected is invoked data.length number of times. Because the lazy selector implementation has O(n) time complexity, this template renders with O(n^2) time complexity - Ugh!. One magnitude of n for the length of data , another for state.selectedIds.length.

Here's one way to improve performance in that example:

@Selector([SelectedDataState])
isDataSelected(state: SelectedDataStateModel) {
  const selectedIds = new Set(state.selectedIds);
  return (id: number) => selectedIds.has(id);
}

Now when the list re-renders, because the lazy selector has O(1) time complexity, this template renders with O(n) time complexity. This optimizes performance by a magnitude of n.

An alternative solution to turning off the selector option is to create a . For example, we declare one selector in our state class and declare another selector outside of our state class:

The above selector is an example of a . This selector returns a function, which accepts an id as an argument and returns a boolean indicating whether or not this id is selected. The lazy selector returned by isDataSelected uses and has O(n) time complexity. In this example, we want to render a list of checkboxes:

The above selector implementation creates a . The lazy selector returned by isDataSelected is a closure with access to the selectedIds variable created in the parent function. The lazy selector uses which has O(1) time complexity.

Selectors
option
meta selector
lazy selector
Array.includes
Set
Set.has