Stop storing server query in NGRX

Please don't use NGRX as server query cache

Let's examine this simple scenario:

  • Angular application
  • with service that fetch a list of github repositories
  • Display the list of repositories in AppComponent
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { GithubService } from "./github.service";
import { CommonModule } from '@angular/common';
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule],
  template: `
    <h1>Repo list</h1>
    <ul>
			@for (repo of repos$ | async; track 'id') {
				<li>
					{{ repo.name }}
				</li>	
			}      
    </ul>
  `,
})
export class AppComponent {
	repos$ = this._github.getRepos();
  constructor(private _github: GithubService) {}
}
Read-only

Everything is working great, but then you have to use the same list of repositories in another component. You could just call the service again, but that would be an unnecessary call and you can simply cache the response (what if you need to use it in 100 places, does that mean you will have to send 100 of those requests?). So you decide to store the list of repositories in NGRX store.
There are few ways that can be achieved (which are all wrong):

  • Dispatch proper actions in components (worst of the wrong solutions)
  • Use @ngrx/effects and make each component dispatch an intent to read that data (a bit better)
  • Use @ngrx/data which will save you a lot of boilerplate code, but still requires you to configure it properly (best of the wrong solutions).

Our recommendation: DON'T USE STATE MANAGEMENT LIBRARY AS SERVER QUERY CACHE

The problem with storing server query in NGRX

If you are using NGRX to store server query, there is a lot of complexity you will have to face. This complexity today has led me to the believe that there is application state, and query cache, and those things should be seperated.
Using NGRX to store server query has lots of boilerplate code, and it is not easy to get it right. In short I would recommend today a side from using state management library (currently NGRX is the most popular) I would recommend to use additional library for sending server queries, which will also cache the responses (I recommend using Angular Query - @ngneat/query).

Let's see what are the problems with storing server query in NGRX.

@ngrx/effects

If you are using @ngrx/effects to store server query, here is all the things you will have to do:

  • create reducer - you can use @ngrx/entity to help you with that (will help you also with selectors)
  • create action to set the list of repositories
  • create effect for acting on components intent to read the data

All the things above will consist of ton of boiler plate code, not to mention that we are not dealing properly with stale data (will be discussed later).

@ngrx/data

The goal of @ngrx/data is to reduce the boilerplate code, and it works nice if your backend api is exactly like the conventions they expect (which 99% is not the case). And then you will have to face configuring @ngrx/data which is not such a simple case and bad documentation makes it even harder to understand how to configure.
Morover, look at this diagram which is posted in @ngrx/data:

@ngrx/data flow

server query cache needs to be simple, most of the developers that are using @ngrx/data I'm not convincede that they fully understand what actually happens, which makes it even harder to understand and debug once there are issues.

The solution - @ngneat/query

There is a very popular data-fetching library for web applications called Tanstack Query. This library can be used in React, Solid, Vue, Svelte and Angular.
@ngneat/query is the Tanstack Query implementation for Angular which is based on the @tanstack/query-core package and similar api to the @tanstack packages, to give you the power of Tanstack Query to Angular.

The main features are:

  • Caching
  • Avoid duplicate requests
  • Update out of date data in background
  • dealing with stale data
  • Managing memory and garbage collection of cached data
  • Dev tools to examine the cache content

In the following example we have 2 components that needs the list of repositories, one which displays the list and another component that display the number of repositories.
Instead of sending 2 queries lets send one query and also define that the data is stale after 30 seconds and needs to be refreshed if there are any changes. This task would have been complex if we did our own solution, but using the power of Tanstack and @ngneat/query it becomes easy.

import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { GithubService } from "./github.service";
import { CommonModule } from '@angular/common';
import { RepoCounterComponent } from './repo-counter.component.ts';

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RepoCounterComponent],
  template: `
    <h1>Repo list</h1>
    <app-repo-counter></app-repo-counter>
    <ul>
      @for(repo of (repos.result$ | async)?.data; track repo.id) {
      <li>
        {{ repo.name }}
      </li>
      }
    </ul>
  `,
})
export class AppComponent {
	repos = inject(GithubService).getRepos();
  
}
Read-only

Notice that on the GithubService I can define the staleTime which means that the data will be stale after 30 seconds, which means it will take the data from the cache but after 30 seconds will also refetch in the background (the user sees the result right away and after refetch his data will be updated).
We also included the query devtools that allows us to inspect the content of our cache.

Conclusion

Using NGRX to store server query is a bad idea, it is complex and hard to get it right. Using a dedicated library for server queries and cache will reduce the amount of complexity and work much better.