Should these states be part of NgRx Store to begin with?

TL; DR: It depends… However, we’ll look into the pros and cons of different approaches. If you chose to make the loading/error as part of the state, make sure they are encompassed within a single property.

Does handling multiple interconnected properties that represent the same AJAX call ✨spark joy✨? If not, then it’s time to let them go and use a single property instead

Not so long ago Michael Hladky started a very interesting discussion on Twitter.

Error is one of the AJAX call states that is frequently placed in the NgRx Store. Others would be Loading and sometimes even Success/Loaded.

interface ResultState {
  result: Result,
  error: string|null,
  isLoading: boolean,
  isLoaded: boolean,
}

Why are these states put into Store? Well, this is often driven by UX decisions. For example, the User should be able to see some kind of progress indicator while waiting for calls to complete, or an error message if that call results in one.

Let’s look into each one of them.

Error State

While I was working on this article, Brandon Roberts already wrote a great article about where to handle error state as a response to that tweet:
Handling Error States with NgRx

In that article he went into great depth looking at how error state can be handled. So here I’ll just summaries his arguments and add a few of my own points.

Error-handling in Effects

Does your Component need to know about the error?

That would be my first question. Does a Component have any logic that is dependent on the error result? It might need to display the error message itself or have some conditional logic that hides/disables parts of the DOM if there is one.

When the only component that needs to handle the error is a snackbar or any other type of pop-up, then Effects can handle that error.

// Dispatch is set to false, so this effect will not try to dispatch
// the result of this effect.
@Effect({ dispatch: false })
handleFetchError: Observable<unknown> = this.actions$.pipe(
  ofType(actions.FETCH_PRODUCTS_ERROR),
  map(() => {
    // Setting the timeout, so that angular would re-run change detection.
    setTimeout(
      () =>
        this.snackBar.open('Error fetching products', 'Error', {
          duration: 2500,
        }),
      0
    );
  })
);

Here is how it looks

Error-handling in Components

When Components need info about errors, one of the possible ways to handle them is directly in the Component itself, without making error as part of the Store state at all. Here we are taking advantage of Actions from the @ngrx/effects package that could be injected into Component, and we could be listening for the Error Action.

@Component({
  selector: 'app-movies-page',
  template: `
    <h1>Movies Page</h1>
    <div *ngIf="error$ | async as error">
      {{ error }}
    </div>
    <button (click)="reload()">Refresh List</button>
  `
})
export class MoviesPageComponent {
  error$: BehaviorSubject<string>;

  constructor(
    private store: Store<fromRoot.State>,
    actions$: Actions,
  ) {
    this.error$ = new BehaviorSubject<string>('');
    actions$.pipe(
      ofType('[Movies/API] Load Movies Failure'),
    ).subscribe(this.error$);
      
   this.store.dispatch({ type: '[Movies Page] Load Movies' });
  }

  reload() {
    this.error.next('');
    this.store.dispatch({ type: '[Movies Page] Load Movies' });
  }
}

The advantages of such an approach are that the error becomes part of the local state of the Component, and as a result, this error is cleaned up when Component is destroyed.

It also helps if we hydrate/rehydrate Store State to and from localStorage via meta-reducer, we wouldn’t be saving/restoring the State with Error. That actually makes sense — we don’t want to show an error to a User, who just opened up the page. Of course, there are some steps that could be taken to prevent that (e.g. filter what parts of State we sync).

However, the number of disadvantages is a lot larger. If there are 2+ Components that rely on that Load Movies response, they all would have to re-implement this error handling logic. What’s making it even more complicated is: when one of those components dispatches another Action to refresh the data, it will only clear its own error. The rest of the Components will continue to display it.

Error in Component B is left behind

Brandon also adds other disadvantages, such as re-introduction of side effect code into your component and a more complicated test setup.

So whether to store the error locally or in the NgRx Store depends on your use case, and the latter might be a safer choice.

Loading State

Spinners or other loading indicators are frequently wired up to isLoading or pending states in NgRx. Some claim that it shouldn’t be stored as a separate property and it is a derived state, which is determined by the presence of the data itself — if it’s not there it might be loading.

const isLoading = createSelector(
    getProductsState, 
    state => !!state.products,
);

However, this is where UX requirements come into play.

Loading while displaying cached results

For example, we have a cached list of products that we want to show while sending an AJAX request to check for any updates. This is the case where just the presence of data in the store is not enough and an explicit isLoading state is necessary.

Subtle indeterminate progress bar indicates to the User that some data might still change

Loading data in chunks

Another example is when a response comes in “paginated” chunks. When the first chunk arrives the data is immediately displayed. But, we would still want to keep showing some kind of a loading indicator until all of the chunks are loaded.

Success/Loaded State

interface ResultState {
  result: Result,
  error: string|null,
  isLoading: boolean,
  isLoaded: boolean,
}

Success state is a tricky one. Personally, I never had to store it; however, I can think of one scenario where it might be useful.

In many cases, the data that I’m working with is a collection of some sort. My result is frequently an array — Result[] . That’s why even when the result comes empty, I can easily distinguish between the initial/loading state (result would be null) and the loaded/success state (result would be [] — empty array).

In the case where the result is a primitive it is still possible to tell the initial/loaded states apart: it’s fine to set the initial state to null for booleans, numbers or strings.

It gets trickier when a result is an object — this is where success state might be used.

The alternative would be to type the result as result: Result|null|{}where an empty object might mean the response came empty. But, there has to be a consensus among team members on what means what.

All-in-one State

With so many additional properties that have to accompany a single AJAX call, could there be a better way?

This is the discussion that we had in our team and the conclusion that we arrived at is that we could combine them all under a single state property, and still get the error message if needed. It also helps eliminate the invalid states, such as { isLoading: true, error: 'Failed', isLoaded: true }

export const enum LoadingState {
    INIT = 'INIT',
    LOADING = 'LOADING',
    LOADED = 'LOADED',
}
export interface ErrorState {
    errorMsg: string;
}

export type CallState = LoadingState | ErrorState;

// Helper function to extract error, if there is one.
export function getError(callState: CallState): string | null { 
    if ((callState as ErrorState).errorMsg !== undefined) { 
        return (callState as ErrorState).errorMsg;
    } 
    return null;
}

With this all-in-one interface our ResultState turns into the following

interface ResultState {
  result: Result,
  callState: CallState,
}

Our reducer becomes cleaner and less prone to errors.

Compare the removed lines with multi-property call state vs added single property one for reducer

Helper getError function can be used in the selectors.

Compare the removed lines with multi-property call state vs added single property one for selectors

As you can see, the selectors stay almost the same while the reducer is dramatically simplified.

Conclusion

While in many cases it is possible to avoid putting error/loading states into the Store, should you have more intricate requirements from UX, consider storing them as a single property.