DEV Community

Tanya Gray
Tanya Gray

Posted on

NgRx Core Concepts for Beginners in 2023

NgRx is a library for Angular which manages a global data store for your app. This article covers some of the core concepts of NgRx:

  • Actions are like events
  • Reducers write data to the state
  • Effects do API calls and map actions to other actions
  • Selectors are for reading data from the state

An Angular app using NgRx will usually have a store directory which should be organised into “sub-stores” for each major data type in the app.

Sub-stores are referred to as store Features. Each Feature like "Users" or "Projects" will have its own directory, and its own Reducer, Actions, Effects and Selectors.

This is a concept guide, not a getting started guide (sorry!). Only small snippets of code are included as examples.


Actions are like events

Actions have unique names, and they are dispatched with some data attached — just like an event.

Actions are defined in groups like this:

const ProjectActions = createActionGroup({
  source: 'Project API',
  events: {
    // defining events with payload using the `props` function
    'Load Project': props<{ projectId: number }>(),
    'Load Project Success': props<{ project: Project }>(),
    'Load Project Failure': props<{ error: Error }>(),
  },
});
Enter fullscreen mode Exit fullscreen mode

NgRx works some magic so that when you use each Action in your code, you refer to it by the type you gave it (the string name / key), but as a camel-cased property of the Action Group.

So from the example above, the Action 'Load Project' would be referred to as ProjectActions.loadProject().

An Action is created and dispatched like this:

const loadProject = ProjectActions.loadProject({ projectId: 123 });
store.dispatch(loadProject);
Enter fullscreen mode Exit fullscreen mode

Actions are just simple objects, so a loadProject action is really just something like this inside:

{
  type: '[Project API] Load Project',
  projectId: 123
}
Enter fullscreen mode Exit fullscreen mode

The type has to be unique across all actions in your app, so it’s common to add a namespaced source like [Project API] to make debugging easier via Redux Dev Tools and to avoid naming conflicts. NgRx does this automatically when you use the createActionGroup function to define your Actions.

In NgRx, both Reducers and Effects can listen for specific Actions.

This means an Action can trigger a Reducer (to change the app state) or trigger an Effect (to go load some data) or it could do both.

Read the official docs for Actions.


Reducers update the state

A Reducer is a collection of functions that each update a small part of the global app state.

Each Reducer function takes in an Action and the current app state, merges the Action’s data into the app state, and returns the new updated app state.

Reducers write data to the global app state, and Selectors read data from the global app state.

A Reducer is usually created like this:

export const projectsReducer = createReducer(
  initialState,

  on(ProjectActions.loadProject, (state) => ({
    ...state,
    loading: true
  })),

  on(ProjectActions.loadProjectSuccess, (state, action) => ({
    ...state,
    loading: false,
    projects: action.projects
  })),

  on(ProjectActions.loadProjectFailure, (state, action) => ({
    ...state,
    loading: false,
    errors: [
      ...state.errors,
      action.error
    ]
  })),
)
Enter fullscreen mode Exit fullscreen mode

The initialState is defined by you, it’s whatever app state you want to start from when the app is first loaded. Each on() function updates the app state when a particular Action happens.

Not every Action needs a matching Reducer, it’s okay to have some Actions which don’t update the app state. A Reducer doesn’t have to process every Action, it can just ignore any Actions it doesn’t care about.

Reducers should ideally do little to no logic. They should rely on being provided clean Actions where the data is already in the correct format.

The state should be immutable, which is why you’ll see Reducers using the ... spread operator to return a copy of the state including the new data, rather than just updating some specific properties.

If an Action’s data needs cleaning, the logic for cleaning should be in a Service and the Action should be processed by an Effect to call that Service.

Read the official docs for Reducers.


Effects map actions to other actions

An Effect usually watches for an Action in your app, executes a function (usually in a Service), and then dispatches another follow-up Action.

An Effect is written as an Observable.

Most Effects look something like this:

loadProject$ = createEffect(() =>
  this.actions$.pipe(
    ofType(ProjectActions.loadProject),
    exhaustMap((action) => 
      this.projectService.getOne(action.projectId).pipe(
        map((project) => ProjectActions.loadProjectSuccess({ project }),
        catchError((error) => of(ProjectActions.loadProjectFailure({ error })))
      )
    )
  )
);
Enter fullscreen mode Exit fullscreen mode

To translate the Effect above: When an action ofType loadProject happens, use exhaustMap to call projectService.getOne(projectId) and then map the result to a new Action — either loadProjectSuccess or loadProjectFailure.

The Action going in is a loadProject.

The Action coming out will be one of loadProjectSuccess or loadProjectFailure.

Sometimes, an Effect may not do an API call, but instead just process an action into one or more other actions, to trigger one or more other Effects.

Very rarely, an Effect may call a function where the result is not important, and so it doesn’t dispatch any kind of success or failure event:

export const loadProjectFailure = createEffect(() =>
  this.actions$.pipe(
    ofType(ProjectActions.loadProjectFailure),
    tap(({ error }) => console.error('Project load failed:', error))
  ),
  { dispatch: false }
);
Enter fullscreen mode Exit fullscreen mode

The example above shows adding { dispatch: false } to make this Effect a “dead end” which will not emit an Action.

Read the official docs for Effects.


Selectors are for getting data

A Selector pulls data out of the global app state. It reads from the big JSON data blob that is maintained by the apps’ Reducers.

Usually, each store Feature defines a selector for the entire Feature state — all the data related to that Feature, like this:

const projectsFeature = createFeatureSelector<ProjectsState>('projects');
Enter fullscreen mode Exit fullscreen mode

That’s because Selectors are composable, so you can make a selector based on another selector. Once you have a selector for a whole Feature, you can drill in deeper to select more specific data.

This is how you select a specific piece of data from a Feature:

export const getAllProjects = createSelector(
  projectsFeature,
  (state: ProjectsState) => state.projects
);
Enter fullscreen mode Exit fullscreen mode

Selectors are Observables, so they emit new values over time as the underlying app state changes.

Selectors can be used from anywhere, but they are most commonly seen in Component code, where they provide data to be used in the template.

A Selector is used in a component’s class like this:

public allProjects$ = this.store.select(getAllProjects);
Enter fullscreen mode Exit fullscreen mode

A Selector’s value can be displayed as JSON-formatted text in a template like this:

<pre>{{ allProjects$ | async | json }}</pre>
Enter fullscreen mode Exit fullscreen mode

Or the value can be passed as an input like this:

<app-projects-list [projects]="$allProjects | async"></app-projects-list>
Enter fullscreen mode Exit fullscreen mode

Read the official docs for Selectors.


There’s a bunch of different “personal preference” styles for writing NgRx apps, but the core concepts remain the same.

Data is handled in a continuous way, where values change dynamically over time based on changes to the app state behind the scenes.

  • Actions are like events, they carry data. They can be used to trigger an Effect, or deliver data to a Reducer.
  • Reducers write data to the state. They run when an Action happens that they’re configured to capture.
  • Effects handle async functionality to map one Action into another. They emit Actions that may contain data for the Reducer to add to the state, or may trigger other Effects.
  • Selectors are for reading data from the state, and their values change dynamically as the underlying app state evolves.

Top comments (0)