Cousins playing nicely: Experimenting with NgRx Store and RTK Query
Tags: redux, angular, ngrx
Published: 2021-04-13 15:14:32

[Redux](https://redux.js.org) provides state management that has been widely used across many different web ecosystems for a long time. [NgRx](https://ngrx.io) provides a more opinionated, batteries-included framework for managing state and side effects in the Angular ecosystem based on the Redux pattern. Redux Toolkit provides users of Redux the same batteries-included approach with conveniences for setting up state management and side effects. The Redux Toolkit (RTK) team has recently released [RTK Query](https://rtk-query-docs.netlify.app/), described as "an advanced data fetching and caching tool, designed to simplify common cases for loading data in a web application", built on top of Redux Toolkit and Redux internally. When I first read the documentation for RTK Query, it immediately piqued my interest in a few ways:

- Simple and Effective
- Built on Redux
- Developer experience focused and built with TypeScript

RTK Query is also **framework-agnostic**, which means it can integrate into _any_ framework. NgRx is a state management framework for Angular based on Redux. So, they _should_ work together, right? This post examines how well they can work together and what I learned while experimenting.

## Setting Up Dependencies

To use RTK Query in an Angular application, some React packages also have to be installed. I installed the common React and Redux toolkit packages.

```ts
npm install @reduxjs/toolkit @rtk-incubator/rtk-query react react-dom react-redux
```

OR 

```ts
yarn add @reduxjs/toolkit @rtk-incubator/rtk-query react react-dom react-redux
```

For additional type information, I installed the `@types/react-redux` package.

```ts
npm install @types/react-redux --only=dev
```

OR 

```ts
yarn add @types/react-redux --dev
```

## Setting up Listeners

RTK Query enables listeners to refetch data on focus, and when reconnecting after going offline.  This involves using the `setupListeners` function and passing the `Store.dispatch` method to it.

```ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { Store, StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';

import { setupListeners } from '@rtk-incubator/rtk-query';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { environment } from '../environments/environment';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    StoreModule.forRoot({}),
    EffectsModule.forRoot([]),
    StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: environment.production })
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
  constructor(store: Store) {
    setupListeners(store.dispatch.bind(store));
  }
}
```

**Note:** Throughout this experiment, I had to use the `.bind` syntax in order to not lose the scope for the Store instance.

## Generating a Products Route

For the feature page, I generated a products module that is lazy-loaded, along with the products component.

```sh
ng g module products --route products --module app
```

## Setting up an API Service

RTK Query creates "API Services" to handle all the data-fetching and state manipulation for the slice of state. These different methods are defined as `endpoints` through the generated service. I defined a `Product` interface, and a `productsApi`  using its `createApi` function.  

```ts
import { createSelector } from '@reduxjs/toolkit';
import { createApi, fetchBaseQuery } from '@rtk-incubator/rtk-query';

export interface Product {
  id: number;
  name: string;
}

export const productsApi = createApi({
  reducerPath: 'products',
  entityTypes: ['Products'],
  baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:3000/' }),
  endpoints: (builder) => ({
    getProducts: builder.query<Product[], string>({
      query: () => 'products',
      provides: (result) => result.map(({ id }) => ({ type: 'Products', id }))
    }),
    addProduct: builder.mutation<Product, Partial<Product>>({
      query: (body) => ({
        url: `products`,
        method: 'POST',
        body,
      }),
      invalidates: ['Products']
    }),
  }),
});
```

**Note:** I used the `json-server` package to setup a quick API for `http://localhost:3000/products` used in this example.

The endpoints use a builder pattern for providing the path to the API URL, querying the API, providing mutations, and cache invalidation for the `Products`. In React, there are provided hooks to abstract away the usage of the `endpoints` that include actions, and selectors but they are provided for usage outside of React.

The `productsApi` object also contains additional properties including the feature key, named `reducerPath`, and `reducer` function itself.

### Defining Actions

The `productsApi` contains defined methods on the `endPoints` property that are essentially action creators. 

```ts
export const loadProducts = () => productsApi.endpoints.getProducts.initiate('');
export const addProduct = (product: Product) => productsApi.endpoints.addProduct.initiate(product);
```

The `initiate` method returns what looks like traditional action creators, but they are actually "Thunks" used in the RTK Query middleware.

### Defining Selectors

Selectors are also defined using the generated `endpoints`. The `getProducts` returns the entire state itself. This integrates seamlessly with the `createSelector` function in NgRx Store to build another selector for the products.

```ts
export const selectProductsState = productsApi.endpoints.getProducts.select('');
export const selectAllProducts = createSelector(
  selectProductsState,
  state => state.data || []
);
```

RTK Query also provides selectors for getting additional information such as the `isLoading`, `isError`, and `isSuccess` statuses which can be combined in the same way.

### Handling Thunks

The first hurdle I ran into is where NgRx Store and Redux start to diverge. A Redux Store provides a synchronous way to get a snapshot of the state, and handling of Redux Thunks. From the Redux Thunk documentation

"Thunks are the recommended middleware for basic Redux side effects logic, including complex synchronous logic that needs access to the store, and simple async logic like AJAX requests."

In order to replicate this API, I created a `ThunkService` that provides the same API necessary to use Redux Middleware.

```ts
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { MiddlewareAPI, ThunkAction } from '@reduxjs/toolkit';
import { SubscriptionLike } from 'rxjs';
import { take } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class ThunkService {
  constructor(private store: Store) {}

  getState() {
    let state: object;

    this.store.pipe(take(1)).subscribe((res) => {
      state = res;
    });

    return state;
  }

  dispatch(thunkAction: ThunkAction<any, any, any, any>): SubscriptionLike {
    return thunkAction(
      (thunk: ThunkAction<any, any, any, any>) =>
        thunk(this.store.dispatch.bind(this.store), this.getState.bind(this), undefined),
      this.getState.bind(this),
      undefined
    );
  }

  runThunk(thunk: ThunkAction<any, any, any, any>) {
    return thunk(
      this.store.dispatch.bind(this.store),
      this.getState.bind(this),
      undefined
    );
  }

  middlewareApi(): MiddlewareAPI {
    return {
      dispatch: this.runThunk.bind(this),
      getState: this.getState.bind(this),
    };
  }
}
```

The `getState` method provides a snapshot of the store, and the `dispatch` method allows for asynchronous dispatching of actions with the provided "thunks".

### Connecting the Middleware

In order to use the Redux middleware, I needed to connect it to the Store. I ended up using a meta-reducer. Meta-reducers by default are just methods that receive a reducer function, and return another reducer function that acts as "middleware" in the NgRx Store. They also don't have access to Dependency Injection unless registered using and `InjectionToken`, so I created an `InjectionToken` for the Products Feature State config.

```ts
export const PRODUCTS_FEATURE_CONFIG_TOKEN = new InjectionToken<StoreConfig<any>>('Products Feature Config');
```

Then I created the factory function to use the `ThunkService`, and a meta-reducer to use the middleware provided by the `productsApi`. When registered, this meta-reducer will only be applied to the `products` slice of state.

```ts
export function productsMiddleware(dispatcher: ThunkService) {
  return function(reducer: ActionReducer<any>): ActionReducer<any> {
    return function(state, action) {
      const next = productsApi.middleware(dispatcher.middlewareApi());
      const nextState = next(action => reducer(state, action));
      
      return nextState(action);
    };
  }
}
```

A factory function is needed to return the defined meta-reducers for the `productsMiddleware` function.

```ts
export function getProductsFeatureConfig(thunkService: ThunkService): StoreConfig<any> {
  return { 
    metaReducers: [productsMiddleware(thunkService)]
  };
}
```

And a provider is needed to register the products feature config.

```ts
export function provideProductsFeatureConfig() {
  return [
    {
      provide: PRODUCTS_FEATURE_CONFIG_TOKEN,
      deps: [ThunkService],
      useFactory: getProductsFeatureConfig,
    }
  ];
}
```

### Registering the feature state

The feature state is setup, and is registered in the `ProductsModule` using familiar syntax with `StoreModule.forFeature()`.

```ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { StoreModule } from '@ngrx/store';

import { ProductsRoutingModule } from './products-routing.module';
import { ProductsComponent } from './products.component';

import { PRODUCTS_FEATURE_CONFIG_TOKEN, productsApi, provideProductsFeatureConfig } from '../services/products';

@NgModule({
  declarations: [ProductsComponent],
  imports: [
    CommonModule,
    ReactiveFormsModule,
    ProductsRoutingModule,
    StoreModule.forFeature(productsApi.reducerPath, productsApi.reducer, PRODUCTS_FEATURE_CONFIG_TOKEN)
  ],
  providers: [
    provideProductsFeatureConfig()
  ],
})
export class ProductsModule { }
```

The `productsApi.reducerPath` is `products` as defined in the API Service, the reducer function is fully generated, the `PRODUCTS_FEATURE_CONFIG_TOKEN` token and `provideProductsFeatureConfig` method are used to register the provider to access dependency injection when registering the meta-reducer. 

### Adding a Product

To add a `Product`, I quickly put together a small form component using Reactive Forms to output the Product after the form is submitted.

```ts
import { Component, OnInit, EventEmitter, Output } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-product-form',
  template: `
    <form [formGroup]="productForm" (ngSubmit)="addProduct()">
      <div>
        Name: <input formControlName="name" type="text">
      </div>

      <div>
        Description: <input formControlName="description" type="text">
      </div>

      <button type="submit">Add Product</button>
    </form>
  `
})
export class ProductFormComponent implements OnInit {
  productForm = new FormGroup({
    name: new FormControl(''),
    description: new FormControl('')
  });

  @Output() submitted = new EventEmitter();

  constructor() { }

  ngOnInit(): void {
  }

  addProduct() {
    this.submitted.emit(this.productForm.value);
  }
}
```

I registered the `ProductsFormComponent` in order to use it in the `ProductsComponent` template.

```ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { StoreModule } from '@ngrx/store';

import { ProductsRoutingModule } from './products-routing.module';
import { ProductsComponent } from './products.component';
import { ProductFormComponent } from './product-form.component';

import { PRODUCTS_FEATURE_CONFIG_TOKEN, productsApi, provideProductsFeatureConfig } from '../services/products';


@NgModule({
  declarations: [ProductsComponent, ProductFormComponent],
  imports: [
    CommonModule,
    ReactiveFormsModule,
    ProductsRoutingModule,
    StoreModule.forFeature(productsApi.reducerPath, productsApi.reducer, PRODUCTS_FEATURE_CONFIG_TOKEN)
  ],
  providers: [
    provideProductsFeatureConfig()
  ],
})
export class ProductsModule { }
```

### Fetching and Adding Products

The state is all wired up, so then I used the state in the `ProductsComponent`. I used the `app-product-form`, and list out the products that are selected from the Store. I also injected the `ThunkService` as a dispatcher for the Action Thunks to be used with the RTK Query middleware.

```ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { SubscriptionLike } from 'rxjs';
import * as uuid from 'uuid';

import { Product } from '../models/product';
import { addProduct, loadProducts, selectAllProducts } from '../services/products';
import { ThunkService } from '../services/thunk.service';

@Component({
  selector: 'app-products',
  templateUrl: './products.component.html',
  styleUrls: ['./products.component.css']
})
export class ProductsComponent implements OnInit, OnDestroy {
  products$ = this.store.select(selectAllProducts);
  loadSub$: SubscriptionLike;
  addSub$: SubscriptionLike;

  constructor(private store: Store, private dispatcher: ThunkService) { }

  ngOnInit(): void {
    this.loadSub$ = this.dispatcher.dispatch(loadProducts());
  }

  onProductAdded(product: Product) {
    this.addSub$ = this.dispatcher.dispatch(addProduct({id: uuid.v4(), ...product}));
  }

  ngOnDestroy() {
    this.loadSub$.unsubscribe();
    this.addSub$.unsubscribe();
  }
}
```

The `products` are selected using the `selectAllProducts` selector, and the `dispatcher` dispatches the Action Thunks provided by RTK Query. If you hadn't noticed, I am not using the `HttpClientModule` or [NgRx Effects](https://ngrx.io/guide/effects) to do any data-fetching or side effects. All this behavior is handled through RTK Query internally. And I still have access to see all the changes in the Redux Devtools.

![NgRx RTK Query Demo](https://media.giphy.com/media/DOFEeSFDqNRf0n8Kng/giphy.gif)

### First Impressions

- RTK Query integrates well with NgRx Store and could potentially be integrated more closely together.
- It was interesting that off-the-shelf Redux Middleware can be connected through a meta-reducer with a little bit of effort. This opens up the possibility to use other Redux Middleware with NgRx Store.

To see the full example, check out the [GitHub Repo](https://github.com/brandonroberts/ngrx-rtk-query)

What do you think? Will you be using RTK Query in your Angular applications? Leave a comment below, or reach out to me on Twitter at [@brandontroberts](https://twitter.com/brandontroberts).

**Note:** While I was going through my experiment, Saul Moro also created a hook-style library for Angular, NgRx, and RTK Query. Check out the repo at:
https://github.com/SaulMoro/ngrx-rtk-query