loading...

Angular service to handle state using BehaviorSubject

alfredoperez profile image Alfredo Perez ・3 min read

Created an abstract service to keep state and handle communication between components and services.

The service uses the BehaviorSubject from RxJS, and have some nice features like auto-completion and being able to get either a snapshot or an observable with the value.

How to use it?

Create a new service extending the PlainStoreService and passing the model of the state.

// Service
@Injectable({providedIn: 'root'})
export class UserEditStore extends PlainStoreService<UserEditState> {

   constructor() {
      super();
      const initialState: UserEditState = {
         user:null,
         isLoading: false,
         isDirty: false,
         isSaving: false
      };
      this.state = new BehaviorSubject<UserEditState>(initialState);
   }
}

Now, you will be able to inject the service wherever is needed and use it.

The service has the following methods:

  • select - Returns an observable for a property on the store.
  • selectSnapshot - Gets the current state of a property
  • set - Set a new value for a single property
  • setState - Sets values for multiple properties on the store

Here is the service:

import { BehaviorSubject, Observable } from 'rxjs';
import { pluck } from 'rxjs/operators';

/**
 * Plain Store Service.
 * This is intended to be used to enable communication between components
 * and it should be just in cases where there are not too many states shared between them.
 */
export class PlainStoreService<T> {

    protected state: BehaviorSubject<T>;

    constructor() {
    }

    /**
     * Returns an observable for a property on the store.
     * This is used when the consumer needs the stream of changes
     * for the property observed.
     *
     * @param key - the key of the property to be retrieved
     */
    public select<K extends keyof T>(key: K): Observable<T[K]> {
        return this.state.asObservable().pipe(pluck(key));
    }

    /**
     * Gets the current state of a property.
     * This is used when the consumer needs just the current state
     *
     * @param key - the key of the property to be retrieved
     */
    public selectSnapshot<K extends keyof T>(key: K): T[K] {
        return this.state.getValue()[key];
    }

    /**
     * This is used to set a new value for a property
     *
     * @param key - the key of the property to be retrieved
     * @param data - the new data to be saved
     */
    public set<K extends keyof T>(key: K, data: T[K]) {
        this.state.next({ ...this.state.value, [key]: data });
    }

    /**
     * Sets values for multiple properties on the store
     * This is used when there is a need to update multiple properties in the store
     *
     * @param partialState - the partial state that includes the new values to be saved
     */
    public setState(partialState: Partial<T>): void {

        const currentState = this.state.getValue();
        const nextState = Object.assign({}, currentState, partialState);

        this.state.next(nextState);
    }

}

Here is the testing file for the service:

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { PlainStoreService } from './plain-store.service';

class MockedStore {
  name: string;
  isEnabled: boolean;
}

@Injectable({providedIn: 'root'})
class MockedStoreService extends PlainStoreService<MockedStore> {

  constructor() {
    super();
    this.state = new BehaviorSubject<MockedStore>(
      {
        name: 'test',
        isEnabled: false
      });
  }
}

describe('PlainStoreService', () => {

  const service = new MockedStoreService();

  it('can load instance', () => {
    expect(service).toBeTruthy();
  });

  describe('select', () => {

    it('should return an observable with the value of the provided key', (done) => {
      service.select('name').subscribe((name) => {
        expect(name).toEqual('test');
        done();
      });
    });

  });

  describe('selectSnapshot', () => {

    it('should return the value of the provided key', () => {
      const name = service.selectSnapshot('name');
      expect(name).toEqual('test');
    });

  });

  describe('set', () => {
    it('should set a new value for a key', () => {
      const newName = 'newValue';

      service.set('name', newName);

      const name = service.selectSnapshot('name');
      expect(name).toEqual(newName);
    });
  });

  describe('setState', () => {
    it('should set a new value for the whole state', () => {
      const newName = 'newValue';
      const newIsEnabled = true;

      service.setState(
        {
          name: newName,
          isEnabled: newIsEnabled
        });

      const name = service.selectSnapshot('name');
      const isEnabled = service.selectSnapshot('isEnabled');

      expect(name).toEqual(newName);
      expect(isEnabled).toBeTruthy();
    });
  });
});

Discussion

pic
Editor guide
Collapse
rattanakchea profile image
Rattanak Chea

Really nice. I wish there is an example I can follow up with.