NgRx Core Concepts for Beginners in 2023
Tags: ngrx, angular
Published: 2023-04-11 04:53:07


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:

```ts
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 }>(),
  },
});
```

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:

```js
const loadProject = ProjectActions.loadProject({ projectId: 123 });
store.dispatch(loadProject);
```

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

```json
{
  type: '[Project API] Load Project',
  projectId: 123
}
```

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](https://ngrx.io/guide/store/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:

```ts
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
    ]
  })),
)
```

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](https://ngrx.io/guide/store/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:

```ts
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 })))
      )
    )
  )
);
```

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:

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

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](https://ngrx.io/guide/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:

```ts
const projectsFeature = createFeatureSelector<ProjectsState>('projects');
```

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:

```ts
export const getAllProjects = createSelector(
  projectsFeature,
  (state: ProjectsState) => state.projects
);
```

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:

```ts
public allProjects$ = this.store.select(getAllProjects);
```
A Selector’s value can be displayed as JSON-formatted text in a template like this:

```html
<pre>{{ allProjects$ | async | json }}</pre>
```

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

```html
<app-projects-list [projects]="$allProjects | async"></app-projects-list>
```

Read the [official docs for Selectors](https://ngrx.io/guide/store/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.