The New NgRx
Tags: angular, ngrx, typescript, rxjs
Published: 2023-02-07 07:36:49

In recent months, [NgRx](https://ngrx.io/), the popular state management library for Angular, has received several significant updates in regards to its API and the way it works. In this atrticle, we will explore what changed, and how this affects the way we use NgRx in our applications.

Main changes are as follows:

- Standalone APIs
- Action groups
- Features
- Extra selectors on features
- Functional effects

Let's dive deeper into each of these updates:

## Standalone APIs

From Angular 14, standalone components are a thing in Angular (stable as of v15). Standalone components interop with `NgModule`-s, so we could just continue using NgRx the way we used too, but the team went on and provided standalone API-s to move away from `NgModule` based architectures. So, instead of this:

```typescript
@NgModule({
  imports: [
    StoreModule.forRoot({

        app: appReducer,
        router: routerReducer
    }),
    EffectsModule.forRoot([AppEffects, LoginEffects])
  ]
})
export class AppModule {}
```

We can now do this:

```typescript
bootstrapApplication(AppComponent, {
    providers: [
        provideStore({
            app: appReducer,
            router: routerReducer
        }),
        provideEffects([AppEffects, LoginEffects])
    ],
});
```

This is not a very significant change, but it's nice to know NgRx grows in sync with latest developments in Angular.

## Action groups

Starting from v14, NgRx includes a helper function to allow us to definte multiple actions at ease, called `createActionGroup`. It works by accepting a source of actions (as in ["good action hygiene"](https://ngrx.io/guide/eslint-plugin/rules/good-action-hygiene)) and a dictionary of events and their respective `props`, so instead of this:

```typescript
export const login = createAction(
  '[Login Page] Login',
  props<{ username: string; password: string }>()
);

export const loginSuccess = createAction(
  '[Login Page] Login Success',
  props<{ user: User }>()
);

export const loginFailure = createAction(
  '[Login Page] Login Failure',
  props<{ error: any }>()
);

export const loginPageOpened = createAction(
    '[Login Page] Login Page Opened'
);
```

We can now do this:

```typescript
export const LoginActions = createActionGroup({
  source: '[Login Page]',
  events: {
    'Login': props<{ username: string; password: string }>(),
    'Login Success': props<{ user: User }>(),
    'Login Failure': props<{ error: any }>(),
    'Login Page Opened': emptyProps(),
  },
});
```

Now, the `LoginActions` object will contain all the actions we defined, so we can use them like this:

```typescript
export class LoginPageComponent implements OnInit {
  constructor(private store: Store) {}

  login(username: string, password: string) {
    this.store.dispatch(LoginActions.login({ username, password }));
  }

  ngOnInit() {
    this.store.dispatch(LoginActions.loginPageOpened());
  }
}
```

Notice that our action types like `Login Failure` written in plain English are converted to `loginFailure` - a camel case variable name. This magic is done using TypeScript's [template literal types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html) and the [mapped types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#handbook-content). The [source code for this](https://github.com/ngrx/platform/blob/master/modules/store/src/action_group_creator_models.ts) is pretty fascinating, if you're a TypeScript enthusiast, I strongly recommend checking it out. 

## Features

For a long time, we introduced new features (usually in lazy loaded modules) with the following pattern:

1. Define a state interface and initial state for the new feature
2. Write a reducer using that initial state
3. Write a feature selector using the `createFeatureSelector` function
4. Use that feature selector to define our selectors with the `createSelector` function, mostly just boilerplate like the following:

```typescript
export const selectFeature = createFeatureSelector<FeatureState>(
    'feature',
);

export const selectFeatureData = createSelector(
  selectFeature,
  (state) => state.data
);
```

5. Then register the reducer in the `forFeature` function of the `StoreModule`:

```typescript
@NgModule({
  imports: [
    StoreModule.forFeature('feature', featureReducer),
  ]
})
export class FeatureModule {}
```

But now, all of this functionality can be reduced into a single function, called `createFeature`, which accepts a `name` and a `reducer` and returns a `Feature` object, which contains the following:

- `reducer`: the reducer we passed in
- selectors: the related selectors deduced from the initial state

So now we can build a feature just like this:

```typescript
const initialState: FeatureState = {
    data: null,
    loading: false,
    error: null,
};
export const Feature = createFeature({
    name: 'feature',
    reducer: createReducer(
        initialState,
        on(
            FeatureActions.loadFeature,
            (state) => ({
                ...state,
                loading: true,
            }),
        ),
        on(
            FeatureActions.loadFeatureSuccess,
            (state, { data }) => ({
                ...state,
                data,
                loading: false,
            }),
        ),
        on(
            FeatureActions.loadFeatureFailure,
            (state, { error }) => ({
                ...state,
                error,
                loading: false,
            }),
        ),
    ),
});
```

So now, we can register the new feature just like this:

```typescript
@NgModule({
  imports: [
    StoreModule.forFeature(Feature),
  ]
})
export class FeatureModule {}
```

And the amazing thing is that we get all the selectors for free, so we can use them like this:

```typescript
@Component({
  selector: 'app-feature',
  template: `
    <div *ngIf="loading$ | async">Loading...</div>
    <div *ngIf="error$ | async">Error!</div>
    <div *ngIf="data$ | async as data">
      <div *ngFor="let item of data">
        {{ item }}
      </div>
    </div>
  `,
})
export class FeatureComponent implements OnInit {
    readonly store = inject(Store);
    readonly data$ = this.store.select(Feature.selectData);
    readonly loading$ = this.store.select(Feature.selectLoading);
    readonly error$ = this.store.select(Feature.selectError);
}
```

As you can notice, the selector was automatically named `selectData` from the `data` property in the initial state. This is done using the same template literal types and mapped types magic we saw in the action groups. 

Now this one is significant, as it very much reduces lots of boilerplate we write when defining new features in an existing store.

## Extra selectors on features

Now the `createFeature` function only creates the default, basic selectors inferred from the data in the initial state. But what if we want to create more selectors? For example, we want to create a selector that returns the data as an array, instead of an object. Previously, we could do this by using the `createSelector` function:

```typescript
export const selectFeatureDataAsArray = createSelector(
  Feature.selectData,
  (state) => Object.values(state.data)
);
```

But this approach slightly decouples this selector from the feature, as it's possible not to define them in the same place. 

But now, we can define extra selectors for a feature by using the `extraSelectors` property of the `createFeature` function:

```typescript
export const Feature = createFeature({
    name: 'feature',
    reducer: createReducer(
        initialState,
        on(
            FeatureActions.loadFeature,
            (state) => ({
                ...state,
                loading: true,
            }),
        ),
        on(
            FeatureActions.loadFeatureSuccess,
            (state, { data }) => ({
                ...state,
                data,
                loading: false,
            }),
        ),
        on(
            FeatureActions.loadFeatureFailure,
            (state, { error }) => ({
                ...state,
                error,
                loading: false,
            }),
        ),
    ),
    extraSelectors: ({selectData}) => ({
        selectDataAsArray: createSelector(
            selectData,
            (data) => Object.values(data)
        ),
    }),
});
```

And now we can use it in our components: 

```typescript
@Component({
  selector: 'app-feature',
  template: `
    <div *ngIf="data$ | async as data">
      <div *ngFor="let item of data">
        {{ item }}
      </div>
    </div>
  `,
})
export class FeatureComponent implements OnInit {
    readonly store = inject(Store);
    readonly data$ = this.store.select(Feature.selectDataAsArray);
}
```

> Note: also use this to combine selectors into view model selectors

## Functional effects:

This one is a fun one: now we do not need to write classes with NgRx at all: oreviously, we needed to create classes to inject our services and the `Actions` `Observable` to create effects and work with them. Now, with the `inject` function, we can just inject the services we need and use them directly in the effect function:

```typescript
export const loadFeature = createEffect(() => {
    const actions = inject(Actions);
    const featureService = inject(FeatureService);
    return actions.pipe(
        ofType(FeatureActions.loadFeature),
        mergeMap(() => featureService.loadFeature().pipe(
            map((data) => FeatureActions.loadFeatureSuccess({ data })),
        )),
        catchError((error) => of(
            FeatureActions.loadFeatureFailure({ error }),
        )),
    );
}, {functional: true});
```

Now we can define a bunch of effects like this, in a file, import them all where we need to register them, and do it:

```typescript
import * as featureEffects from './users.effects';

bootstrapApplication(AppComponent, {
  providers: [provideEffects(featureEffects)],
});
```

You can also shorten this a bit by providing the dependencies as default arguments when creating the effect:

```typescript
export const loadFeature = createEffect(
    (
        actions = inject(Actions),
        featureService = inject(FeatureService),
    ) => actions.pipe(
        ofType(FeatureActions.loadFeature),
        mergeMap(() => featureService.loadFeature().pipe(
            map((data) => FeatureActions.loadFeatureSuccess({ data })),
        )),
        catchError((error) => of(
            FeatureActions.loadFeatureFailure({ error })),
        ),
    ),
    {functional: true},
);
```

## Implications

As a result of these changes, folder structure may be affected. If previously we had something like this:

```bash
└── store/
    ├── reducers/
    │   ├── app.reducer.ts
    │   ├── feature.reducer.ts
    │   └── other.reducer.ts
    ├── actions/
    │   ├── app.actions.ts
    │   ├── feature.actions.ts
    │   └── other.actions.ts
    ├── selectors/
    │   ├── app.selectors.ts
    │   ├── feature.selectors.ts
    │   └── other.selectors.ts
    └── effects/
        ├── app.effects.ts
        ├── feature.selectors.ts
        └── other.selectors.ts
```

But now, with the `createFeature` capability, we can also reduce our folder boilerplate by grouping all the files related to a feature in a single folder:

```bash
└── store/
    ├── features/
    │   ├── app.feature.ts
    │   ├── feature.feature.ts
    │   └── other.feature.ts
    ├── actions/
    │   ├── app.actions.ts
    │   ├── feature.actions.ts
    │   └── other.actions.ts
    └── effects/
        ├── app.effects.ts
        ├── feature.selectors.ts
        └── other.selectors.ts
```

Also, this means less confusion between feature selectors, improved unit testing, and, of course, again, less boilerplate.

## In Conclusion

NgRx is, as all Angular ecosystem, a rapidly developing library, and its advance means a brighter future for Angular projects.