Mike Ryan is one of the authors of NgRx. He has a YouTube video “You might not need NgRx”, which showcases when you can use NgRx. Some people just don’t care about those use cases and use it in the wrong scenarios, writing unnecessarily complex code, which makes others hate NgRx more. While it is important to document when to use it, and how to use it, it is equally important to document when you should not use it. In this post, I am trying to document some of the anti-patterns in using NgRx.

All the source code of the post can be found here. The live demo is here

1. Use NgRx store for temporary variables.

NgRx store is for state management. But sometimes people use it for non-state management. So what is state in terms of a Single Page Application? I can’t find a definite answer. So here is my definition.

If a variable last after a function execution is finished, it is a state.

In terms of an Angular application, the state can be a property of a component or a property of services. But the bottom line is that it should be shared and lasting.

Here is a use case. A user enters a token in a text box, the application validates it by calling a remote service, and later displays the result. This is a very simple use case. But NgRx is so cool, let’s use it here. First, let’s write the following code to implement the NgRx store.

export const antiPatternActions = createActionGroup({
  source: "anti pattern",
  events: {
    "token updated": props<{ token: string }>(),
    validate: props<{ token: string }>(),
    "validate success": props<{ isValid: boolean }>(),
  },
});

export const antiPatternFeature1 = createFeature({
  name: "antiPattern",
  reducer: createReducer(
    {
      token: "",
      isValid: false,
    },
    on(antiPatternActions.tokenUpdated, (state, { token }) => {
      return { ...state, token };
    }),
    on(antiPatternActions.validateSuccess, (state, { isValid }) => {
      return { ...state, isValid };
    })
  ),
});

@Injectable({
  providedIn: "root",
})
export class AntiPatternEffects {
  constructor(private actions$: Actions, private api: XApi) {}

  validate$ = createEffect(() =>
    this.actions$.pipe(
      ofType(antiPatternActions.validate),
      concatMap(({ token }) => {
        return this.api.validate(token);
      }),
      map((result) => {
        return antiPatternActions.validateSuccess({ isValid: result });
      })
    )
  );
}

export const antiPatternModule1 = [
  StoreModule.forFeature(antiPatternFeature1),
  EffectsModule.forFeature(AntiPatternEffects),
];

After that, we can consume the state in the component.

@Component({
  selector: "app-anti-pattern1",
  template: `
    <div>
      <h1>anti pattern: use store for temporary value</h1>
      <p>token: <input [formControl]="txtToken" /> (valid value is 'super')</p>

      <p>isValid: {{ isValid$ | async }}</p>
    </div>
  `,
  styles: [],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AntiPattern1Component implements OnDestroy {
  private destroy$ = new Subject<void>();

  constructor(private store: Store) {
    // when input change, save it to token slot in store
    this.txtToken.valueChanges
      .pipe(takeUntil(this.destroy$))
      .subscribe((token) => {
        this.store.dispatch(antiPatternActions.tokenUpdated({ token }));
      });

    // when token slot change, trigger to side effect to validate it
    this.store
      .select(antiPatternFeature1.selectToken)
      .pipe(takeUntil(this.destroy$))
      .subscribe((token) => {
        this.store.dispatch(antiPatternActions.validate({ token }));
      });
  }
  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  txtToken = new FormControl();

  // subscribe isValid slot in store
  isValid$ = this.store.select(antiPatternFeature1.selectIsValid);
}

Here is what we want to do:

  1. Observe the change to the user’s input, when it changes, save it to state.token.
  2. Observe the change to state.token, when it changes, trigger an effect, which will call the API method, when the API data returns, raise an action, which will trigger the reducer to update state.isValid.
  3. Display state.isValid on the page when it is updated.

It looks like we have achieved a lot by writing lots of code. But the token and isValid are some private states of the component. They are not meant to be shared at all. In fact, we don’t need an extra variable state to hold the token, because the user’s input is the token.

Yeah, yeah, yeah. You may laugh at the code now. You may say, NgRx is over-engineered. No. We simply used it in the wrong use case. How much code can you save if we don’t use NgRx? Here is the code.

@Component({
  selector: "app-anti-pattern1-fix",
  template: `
    <div>
      <h1>fix for anti pattern: use store for temporary value</h1>
      <p>token: <input [formControl]="txtToken" /> (valid value is 'super')</p>
      <p>isValid: {{ isValid$ | async }}</p>
    </div>
  `,
  styles: [],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AntiPattern1FixComponent {
  constructor(private api: XApi) {}

  txtToken = new FormControl();

  isValid$ = this.txtToken.valueChanges.pipe(
    switchMap((value) => this.api.validate(value))
  );
}