personal blog on code


Having Fun with State in Angular

Conquer the Dark Side of Error, Loading and Routing State

The last couple of weeks there was a little discussion going on about handling loading and error state in NgRx. Two blog posts were written about it shortly and I highly recommend looking into them if you are using NgRx:

I have a different opinion on this topic. I want to take off the NgRx glasses and take a look at the problem from a different point of view. Have fun. Please note that the following is a very opinionated post based on my experience. Feel free to discuss it :)

State management is one of the hardest things to get right. At some point in a career of every software developer, one learns the magic words:

"It depends"

And maybe it truly does, but: I’m done with “It depends!” 🤭

Sometimes I have the feeling we are missing the point and start discussing things with a very limited view based on one framework and a very specific library.

I think we should not ask what’s the best way to handle loading or error state with NgRx. Independently of a state management library or not, our questions should be more generic.

Do we show the errors of our application in one place or do we show them dependent on the component inside of it? Should it be in the store in the first place?

Dan, Creator of the Redux library, can help us with this decision:

If you’re not sure whether some state is local, ask yourself: “If this component was rendered twice, should this interaction reflect in the other copy?” Whenever the answer is “no”, you found some local state. — Dan Abramov https://overreacted.io/writing-resilient-components/

In my opinion loading- and error states could be more often local state than global state! Even if we show our errors only in one place of our application, it is not shared state. Do I need a single source of truth then? I would even argue that there is already one, it's just not represented in the NgRx state for example.

In the following example, we are going to look not only at the error state, but we will also try to demystify the marvelous loading state and router state. Ever had problems with them? I had them for sure 🐼

Let’s get this party started: In our example, we build a simple GitHub commits search view. You can enter the name of your favorite developer and see the last commits she/he has done in public repositories.

alt text

The Error State 🆘

When we enter a wrong GitHub username we want to display an error message beneath the input field. Just like that:

alt text

There are different options how we can accomplish this behaviour:

1. Managed in the component

The first approach is to handle the error state in the component itself. We are not using any kind of state management library. This is pretty straight forward. Following template renders our view:

<card centered> 
  <card-header avatar="github-avatar">
    <card-header-title>Github Commits</card-header-title>
    <card-header-subtitle>Search</card-header-subtitle>
  </card-header>
  <github-search-form (onSubmit)="username = $event"></github-search-form>
  <github-search-result>
    <!-- Load commits with the commits container -->
    <commits-with-service-container [username]="username" #container>
      <commits [commits]="container.commits"></commits>
      <failure [failures]="container.failures"></failure>
    </commits-with-service-container>
  </github-search-result>
</card>

I‘ve tried to write a very declarative template in order to keep the example small and readable. I basically wrapped the directives from Angular Material within custom components. There are pros and cons in this approach but that should not bother us for now.

In the corresponding container <commits-with-service-container> we inject the CommitsService and call the readCommitsByUsername method on every change of the username input. This triggers an HTTP request. We can then pass the response with the commits or failures to our presentational components <commits> and <failure> with the help of a template reference variable.

This may seem like a strange container component for you, and I partly agree, but it does everything a container component should do in my opinion:

  • It should do only one thing
  • It should be reusable
  • It should not have a template for itself
  • It should not have any styles attached

Therefore we get a highly reusable, testable and solid component. Further, we can decide whether we like to propagate the failures to a custom failure component or handle it directly within the container via a direct service call to some FailureService. In our example, we delegate the failures to our <failure> component.

@Component({
  selector: 'commits-with-service-container',
  template: '<ng-content></ng-content>',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CommitsWithServiceContainer implements OnChanges {

  @Input() username: string;

  commits: Commit[] = [];
  failures: Failure[] = [];

  constructor(private commitsService: CommitsService) { }

  ngOnChanges({username}: SimpleChanges) {
    onChange(username, this.loadCommmits);
  }

  loadCommmits = (username: string) => {
    this.commitsService.readCommitsByUsername(username)
      .then(this.handleSuccess)
      .catch(this.handleFailure)
  }

  handleSuccess = (commits: Commit[]) => {
    this.failures = [];
    this.commits = commits;
  }

  handleFailure = () => {
    this.commits = [];
    this.failures = [new Failure('Oh dear! Something went wrong, we are sorry!')];
  };
}

The onChange function call within the ngOnChanges lifecycle hook is just a little helper function that checks whether the username has changed, calling the given callback if it has. You can take a look at it here.

2. Managed in the store

The second approach is to manage the error state within the NgRx store. This is the way we would handle our errors if we decided to keep it in a central store. Therefore we create a global object, which represents our state:

export interface CommitsState {
  errors: Failure[];
  commits: Commit[];
}

The template of the view stays more or less the same. Only the container components changes:

<card centered> 
  <card-header avatar="github-avatar">
    <card-header-title>Github Commits</card-header-title>
    <card-header-subtitle>Search</card-header-subtitle>
  </card-header>
  <github-search-form (onSubmit)="username = $event"></github-search-form>
  <github-search-result>
    <commits-with-ngrx-container [username]="username" #container>
      <commits [commits]="container.commits$ | async"></commits>
      <failure [failures]="container.failures$ | async"></failure>
    </commits-with-ngrx-container>
  </github-search-result>
</card>

In the container, we do not handle the error case any longer. We just pass the observables that we get from our store selectors to the template, which handles the subscription for us automatically.

@Component({
  selector: 'commits-with-ngrx-container',
  template: '<ng-content></ng-content>',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CommitsWithNgrxContainer implements OnChanges {

  @Input() username: string;

  commits$: Observable<Commit[]>;
  failures$: Observable<Failure[]>;

  constructor(private store: Store<AppState>) {
    this.commits$ = this.store.select(commits);
    this.failures$ = this.store.select(commitsFailuerState);
  }

  ngOnChanges({username}: SimpleChanges) {
    onChange(username, this.loadCommits);
  }

  loadCommits = (username) => {
    this.store.dispatch(new LoadCommits({ username }));
  }
}

The logic with the error state handling is now placed within the reducer function. There we have to make sure to set and reset the error state correctly:

export function reducer(
  state = initialState, 
  action: Commits.CommitsActionsUnion
): State {
  switch (action.type) {
    case Commits.CommitActionTypes.LoadCommits: {
      return {
        ...state,
        commits: [],
        errors: []
      };
    }

    case Commits.CommitActionTypes.LoadCommitsSuccess: {
      return {
        ...state,
        commits: action.payload.commits
      };
    }

    case Commits.CommitActionTypes.LoadCommitsFailure: {
      return {
        ...state,
        errors: action.payload.errors
      };
    }

    default:
      return state;
  }
}

3. Action Selectors

The third approach is to listen to the error actions of NgRx in the container component itself. Here we make use of the Actions service from NgRx-Effects, but that's just because it's convenient. We could have written our own Redux middleware (metareducer in NgRx) and be good with that.

@Component({
  selector: 'commits-with-ngrx-action-listener-container',
  template: '<ng-content></ng-content>',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CommitsWithNgrxActionListenerContainer implements OnChanges {

  @Input()
  username: string;

  commits$: Observable<Commit[]>;
  failures$: Observable<Failure[]>;

  constructor(private store: Store<AppState>,
              private actions$: Actions<CommitsActionsUnion>) {
    this.commits$ = this.store.select(commits);
    this.failures$ = this.actions$.pipe(selectFailures(CommitActionTypes.LoadCommits));
  }

  ngOnChanges({username}: SimpleChanges) {
    onChange(username, this.loadCommits);
  }

  loadCommits = (username) => {
    this.store.dispatch(new LoadCommits({ username: username }));
  }
}

Michael Hladky has shown this approach already in one of his examples. I just wrapped it in a custom RxJs operatorselectFailures:

// custom RxJs operator
export const selectFailures = (actiontype: string) => (source: Observable<any>) => merge(
  source.pipe(ofType(actiontype), mapTo([])),
  source.pipe(ofType(actiontype + ' Failure'), map((action: any) => action.payload.errors))
);

In my point of view derived state is the right word to explain this. The selectFailures operator can be compared with a “normal” selector you are all familiar with. Just that it does not select from the state of the store. It selects on actions and derives state from it. There is basically no need to save this in a store and therefore the boilerplate in the reducer is reduced. I coined the term Action Selectors for this kind of selectors 🤓

The Loading State 💬

I think loading state is local state and if not it should be! In the examples above I left out the loading state on purpose. Most of the time loading state and error state are treated together. That's why they often end up in the store together as well.

Why is loading state a local state? In our example above the loading state could be handled application wide, e.g with a loading spinner that spans over the whole screen or local to the form with a spinner just above the form. The best indication for a local state is the desired behavior that we would want if we had decided to display two independent search forms next to each other. Should their loading state block each other, when one is loading commits?

Update: Alex pointed out that I kind of contradict the RemoteData approach in the following section, where I put together the loading and error state in one place. I tried to separate the error part from the loading part only for this writeup. I think both should be treated together. If you decide to keep to loading state local the error state should be local too and vice versa.

If we handle the loading state in the commits container we can again decide in which way we want to handle the loading state. We went with a delegation in our example. In the template we added the loading component and fed it with the loading state from the container:

<card centered> 
  <card-header avatar="github-avatar">
    <card-header-title>Github Commits</card-header-title>
    <card-header-subtitle>Search</card-header-subtitle>
  </card-header>
  <github-search-form (onSubmit)="username = $event"></github-search-form>
  <github-search-result>
    <commits-with-ngrx-action-listener-container [username]="username" #container>
      <commits [commits]="container.commits$ | async"></commits>
      <failure [failures]="container.failures$ | async"></failure>
      <loading [isLoading]="container.loading$ | async"></loading>
    </commits-container>
  </github-search-result>
</card>

The container is extended with an action selector selectLoading:

export const selectLoading = (actiontype: string) => (source: Observable<any>) => merge(
  source.pipe(ofType(actiontype), mapTo(true)),
  source.pipe(ofType(actiontype + ' Failure'), mapTo(false)),
  source.pipe(ofType(actiontype + ' Success'), mapTo(false))
);

The actiontype is either:

  • Load Commits
  • Load Commits Success
  • Load Commits Failure

On Load Commits we map to the loading state true.

On Load Commits Success and Load Commits Success we map to the loading state false.

@Component({
  selector: 'commits-with-ngrx-action-listener-container',
  template: '<ng-content></ng-content>',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CommitsWithNgrxActionListenerContainer implements OnChanges {

  @Input()
  username: string;

  commits$: Observable<Commit[]>;
  failures$ = new BehaviorSubject([] as Failure[]);
  loading$ = new BehaviorSubject(true);

  constructor(private store: Store<AppState>,
              private actions$: Actions<CommitsActionsUnion>) {
    this.commits$ = this.store.select(commits);

    this.actions$.pipe(selectFailures(CommitActionTypes.LoadCommits)).subscribe(this.failures$);
    this.actions$.pipe(selectLoading(CommitActionTypes.LoadCommits)).subscribe(this.loading$);
  }

  ngOnChanges({username}: SimpleChanges) {
    onChange(username, this.loadCommits);
  }

  loadCommits = (username) => {
    this.store.dispatch(new LoadCommits({ username: username }));
  }
}

Please note that it is important to subscribe to the action selectors before you dispatch them in order to be sure not to miss any action event. In the example above it is guaranteed that the subscription in the constructor runs before ngOnChanges triggers and so before the LoadCommits action is dispatched.

Avoiding impossible states

There is another way of dealing with remote data and its state. The idea is coming from the elm language and it was first written down by Kris Jenkins in his writeup: How Elm Slays a UI Antipattern

The key takeaway from it is the common UI antipattern, where the state of an ongoing async operation is modeled like the following:

const data = {
    loading: true,
    commits: []
}

On a slow network connection, where the loading of the commits lasts a bit longer this can become problematic. Then sometimes applications already show a message, based on the empty commits array, that they didn’t find any commits. This is wrong though because the data is just being fetched. This happens because of the very convenient practice of developers to initialize lists with empty arrays in order to avoid undefined checks. This leads to a state where we are no longer able to differentiate between an empty result and an initial value.

Our GitHub commits search example from above had a similar problem:

alt text

There is no user yanx2, but because I set an empty array for the commits in the error case I could no longer differentiate between a user that just had no commits done and a user that does not exist at all.

To solve this problem we can introduce a custom type RemoteData, which is a union type of four types :


export type Success<S> = { 
  success: S;
}

export type Failure<F> = {
  failure: F;
}

export type NotAsked = {
  notAsked: true;
}

export type Loading = {
  loading: true;
}

export type RemoteData<S, F> 
  = NotAsked 
  | Loading
  | Success<S>
  | Failure<F>;
//
//
export const notAsked: NotAsked = {
  notAsked: true
};

export const loading: Loading = {
  loading: true
};

export function success<S>(success: S): Success<S> {
  return {
    success
  };
}

export function failure<F>(failure: F): Failure<F> {
  return {
    failure
  };
}

This was just my first take on typing RemoteData. You can of course also use classes instead of types or just use the already existing library ngx-remotedata.

@Component({
  selector: 'commits-with-remote-data-container',
  template: '<ng-content></ng-content>',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CommitsWithRemoteDataContainer implements OnChanges {

  @Input()
  username: string;

  commits: RemoteData<Commit[], Failure[]> = notAsked;

  constructor() { }

  ngOnChanges({username}: SimpleChanges) {
    onChange(username, this.loadCommits);
  }

  loadCommits = (username) => {
    this.commits = loading;
    readCommitsByUsername(username)
      .then(this.handleSuccess)
      .catch(this.handleFailure);
  }

  handleSuccess = (commits: Commit[]) => {
    this.commits = success(commits);
  }

  handleFailure = () => {
    this.commits = failure([new Failure('Oh dear! Something went wrong, we are sorry!')]);
  }
}

The initial state of the commits is notAsked. When we are loading the commits we set it to loading and then either to success or failure. In the template, we then access the RemoteData type.


<card centered> 
  <card-header avatar="github-avatar">
    <card-header-title>Github Commits</card-header-title>
    <card-header-subtitle>Search</card-header-subtitle>
  </card-header>
  <github-search-form [username]="username$ | async" (onSubmit)="search($event)"></github-search-form>
  <github-search-result>
    <commits-with-remote-data-container [username]="username$ | async" #container>
      <commits [commits]="container.commits.success"></commits>
      <failure [failures]="container.commits.failure"></failure>
      <loading [isLoading]="container.commits.loading"></loading>
    </commits-with-remote-data-container>
  </github-search-result>
</card>

The RemoteData datatype can be used with a state management library as well. Then you would set the different states within the reducer. You can take a look at this in the example repo.

The Router State

Router state handling can become cumbersome really quickly in combination with the use of a state management library and effects.

1. NgRx Effects

I think we should not handle routing concerns within effects. I have done it myself in the past and after some time I realized that it was not my best decision. Suddenly I had effects that listened to URL change events and some who listened to success events of async HTTP calls in order to be able to navigate to a different route. In a simple application, this may be sufficient but later when the application grows there is a point where it becomes hard to understand which event triggered the next or why some event was triggered at all.

Routing should be done by the view or the container component! If you have the feeling that it is the best to handle your routing concerns within effects you most likely have a problem with a proper separation of container-, view- and presentational components.

In our example the CommitsView is in charge of handling the routing. It listens to the route param from our routecommits/:usernameParam and navigates to it on search.

@Component( {
  selector: 'commits-view',
  templateUrl: 'commits-view.html',
  styleUrls: ['commits-view.scss']
})
export class CommitsView {

  username$: Observable<string>;

  constructor(private router: Router,
              private route: ActivatedRoute) {
    this.username$ = this.route.params.pipe(map(params => params['usernameParam']));
  }

  search(username: string) {
    this.router.navigate(['commits', username]);
  }
}
<card centered> 
  <card-header avatar="github-avatar">
    <card-header-title>Github Commits</card-header-title>
    <card-header-subtitle>Search</card-header-subtitle>
  </card-header>
  <github-search-form [username]="username$ | async" (onSubmit)="search($event)"></github-search-form>
  <github-search-result>
    <commits-with-ngrx-action-listener-container [username]="username$ | async" #container>
      <commits [commits]="container.commits$ | async"></commits>
      <failure [failures]="container.failures$ | async"></failure>
      <loading [isLoading]="container.loading$ | async"></loading>
    </commits-with-ngrx-action-listener-container>
  </github-search-result>
</card>

2. When to use NgRx Router Store?

The router implementation already keeps track of its own state, so I would argue to try not to duplicate it. If there is already a username parameter in the route then there is no need to duplicate that state in a central store. Access these parameters in your container or view component and pass it via input properties or action payloads if necessary.

The only reason we might want to use NgRx Router Store is when we want to be able to record and replay actions and in this case, we don’t want to lose the navigational router events. That’s all.

Update: Tim Deschryver pointed out that the NgRx-Router-Store is great for accessing routing parameters within selectors. This is a valid approach I forgot to mention.

Summary

This was a long journey and it took quite a while to write it down. It is a very opinionated post and I had a hard time to structure it, but I didn’t want to leave out any thoughts I had regarding this topic. Feel free to discuss it in the comments.

Feal free to reach out to me on Twitter. Sources to the example.