Creating a Data Store in Angular

bob.ts - Sep 13 '21 - - Dev Community

Late last night (actually, early this morning), I had visions of dots dancing in my head: the dots and lines used to describe actions and their relation to data in ReactJS data stores ... and the dots and lines used to describe data movement and management of Observables and subscriptions.

I sprang from my bed ... getting up VERY early as these 'dots' whirled around in my head and put this code (repo) and article together.

Having worked with Angular for quite a while, I've come across a few patterns that help improve my code quality and finally came up with a way to show how I have implemented a ReactJS-like data store.

If you are not familiar with React data stores, basically, it has a method that uses actions (whether they are user, event, or data-driven) to trigger functionality related to data and have the application watching for these changes and be able to change the view.

Concept

This code is designed around a data store where all actions within the application pass. This has a few advantages:

  1. It provides a Single Source of Truth for the application's data and states.
  2. It centralizes the process of triggering actions, giving a clean accounting of what's happening (one console.log to show them all).
  3. It allows for a location for "global" functionality, such as a spinner when an API request is in-flight.
  4. It provides a central location for all components and services to tie into Observables via Subjects to see data when it changes, rather than passing data around.

Specifically for the last advantage (#4), this allows code to be developed that is not constantly ...

  • Passing data down the "tree" of components via attributes, [data]="data".
  • Or event worse, passing a function down so that we can tell the parent(s) that the data has changed in some way, [updatingDataFn]="updatingData.bind(this)".

This code shows several variations to both data and state management.

Actions

First, here is the code to define a few actions ...

import { Injectable } from '@angular/core';

import { Actions } from '../interfaces/actions';

@Injectable({
  providedIn: 'root'
})
export class ActionsService {

  constants: Actions = {
    CHANGE_WEATHER_UNIT: 'CHANGE_WEATHER_UNIT',

    INITIATE_WEATHER: 'INITIATE_WEATHER',
    TRIGGER_WEATHER: 'TRIGGER_WEATHER',
    RECEIVED_WEATHER_DATA: 'RECEIVED_WEATHER_DATA',

    TOGGLE_ICON: 'TOGGLE_ICON'
  };

}
Enter fullscreen mode Exit fullscreen mode

In this case, I used a service and within my code have to reference this as actionService.constants. This could easily have been a JSON file and with imported constants; either would have been sufficient.

There are three evident things that are going to occur based on these constants:

  1. Changing the weather unit (Imperial (F) or Metric (C)).
  2. Initiate, trigger, and receive weather data (initiate sets up a one minute setInterval so that the data trigger fires over and over).
  3. Toggle icon simply changes the favicon.

Basically, this code should show that an api can be called with optional configuration (the units) and see the changes applied. Also, it shows a way to directly change a value ... this is a bit roundabout, but has further implications when that data needs to be shared throughout the application (across components or within other services).

Data Store

The basic store is similar in functionality to what I've used in ReactJS.


import { Injectable } from '@angular/core';
import { BehaviorSubject, Subject } from 'rxjs';

import { Actions } from '../interfaces/actions';
import { TempAndIcon, Units } from '../interfaces/temp-and-icon';
import { ActionsService } from './actions.service';

import { IconStateService } from './icon-state.service';
import { WeatherApisService } from './weather-apis.service';

@Injectable({
  providedIn: 'root'
})
export class DataStoreService {

  private actions: Actions;

  public iconState: BehaviorSubject<boolean> = new BehaviorSubject(this.icon.initialState);

  public weatherData: Subject<TempAndIcon> = new Subject();

  private _weatherUnit: Units = 'imperial';
  public weatherUnit: BehaviorSubject<Units> = new BehaviorSubject(this._weatherUnit);

  private _spinner: boolean = false;
  public spinner: BehaviorSubject<boolean> = new BehaviorSubject(this._spinner);

  constructor(
    private actionsService: ActionsService,
    private icon: IconStateService,
    private weather: WeatherApisService
  ) {
    this.weather.setActionRunnerFn = this.processAction.bind(this);
    this.actions = this.actionsService.constants;
  }

  processAction = async (action: string, data: any) => {
    console.log(action, data);
    switch (true) {
      case (action === this.actions.CHANGE_WEATHER_UNIT):
        this._weatherUnit = data;
        this.weatherUnit.next(this._weatherUnit);
        break;

      case (action === this.actions.INITIATE_WEATHER):
        this.weather.initiateWeather();
        break;
      case (action === this.actions.TRIGGER_WEATHER):
        this.spinner.next(true);
        this.weather.getWeather(this._weatherUnit);
        break;
      case (action === this.actions.RECEIVED_WEATHER_DATA):
        this.weatherData.next(data);
        this.spinner.next(false);
        break;

      case (action === this.actions.TOGGLE_ICON):
        const newState = this.icon.toggleState(data);
        this.iconState.next(newState);
        break;
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

Here, there are Subject and BehaviorSubject declarations (determining which to use is simple: do you know the initial state or not). These are what the components and services can subscribe to, watch for data changes, and effect change because of that data.

The processAction function takes an action and data and executes expected functionality.

NOTE also that there is a spinner defined; this could be used to efficiently turn a spinner on and off in the DOM.

Handling the Favicon

Within a component, boolean value it toggled resulting in the system displaying a different favicon.

  iconState: boolean = true;
  favIcon: HTMLLinkElement = document.querySelector('#appIcon')!;
  ...
  constructor(
    ...,
    private dataStore: DataStoreService
  ) {
    ...
    this.dataStore.iconState.subscribe((data: boolean) => {
      this.iconState = data;
      this.favIcon.href = (data === true) ? '/assets/icons/sunny.ico' : '/assets/icons/dark.ico';
    });
  }
Enter fullscreen mode Exit fullscreen mode

The actual "toggle" is as follows ...

  toggleFavicon = () => {
    this.dataStore.processAction(this.actions.TOGGLE_ICON, this.iconState);
  };
Enter fullscreen mode Exit fullscreen mode

Basically, this code is firing the processAction function seen earlier and passing the state. Within the constructor, the subscription allows the code to change the icon href location on state changes.

Handling the Weather Units

Here, radio buttons are used to change between Fahrenheit and Celsius. This code shows a difference pattern from the toggle code for the icon, seen previously ...

  units: Units = 'imperial';

  constructor(
    ...,
    private dataStore: DataStoreService
  ) {
    ...
    this.dataStore.weatherUnit.subscribe((data: Units) => {
      this.units = data;
    });
  }

  unitChange = (value: Units) => {
    this.dataStore.processAction(this.actions.CHANGE_WEATHER_UNIT, value);
  };
Enter fullscreen mode Exit fullscreen mode

Again, there is a subscription that simply updates the locally stored units. In the HTML, (change)="unitChange($event.value)" is used to trigger the change function, passing the selected value. Within the called function, the action and value are passed the to the store as seen previously.

Displaying a Weather Icon

This is simple code ... there is an <img> tag with [scr]="source". The following code sets the source value.

  source: string = '';

  constructor(
    private dataStore: DataStoreService
  ) {
    this.dataStore.weatherData.subscribe((data: TempAndIcon) => {
      this.source = data.icon;
    });
  }
Enter fullscreen mode Exit fullscreen mode

The subscription seen here is used again in the next set of code, again with a slightly different variation on the data used.

Displaying Temperature with Units

First, the HTML ...

<div class="temperature">
  {{ temperature }}
  {{ units === 'imperial' ? 'F' : 'C' }}
</div>
Enter fullscreen mode Exit fullscreen mode

Now, take a look at how this data is set and managed ...

  temperature: number = -1;
  units: Units = 'imperial';

  constructor(
    private dataStore: DataStoreService
  ) {
    this.dataStore.weatherData.subscribe((data: TempAndIcon) => {
      this.temperature = data.temp;
      this.units = data.units;
    });
  }
Enter fullscreen mode Exit fullscreen mode

Here, the code inside the subscribe is setting two values when things change.

The API Service

This is the Weather API Service used ... the API Key is hidden ... to run the code, go to OpenWeathermap, create an account, and swap this one with your own key.


import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Actions } from '../interfaces/actions';

import { ActionsService } from './actions.service';
import { TempAndIcon, Units } from '../interfaces/temp-and-icon';

@Injectable({
  providedIn: 'root'
})
export class WeatherApisService {

  private actions: Actions;

  private interval: number = 1000 * 60;
  public setActionRunnerFn: any;

  constructor(
    private actionsService: ActionsService,
    private http: HttpClient
  ) {
    this.actions = this.actionsService.constants;
  }

  initiateWeather = () => {
    setInterval(this.triggerActionRunner, this.interval);
    this.triggerActionRunner();
  };

  triggerActionRunner = () => {
    this.setActionRunnerFn(this.actions.TRIGGER_WEATHER, null);
  };

  getWeather = async (unit: Units) => {
    const url: string = `http://api.openweathermap.org/data/2.5/weather?id=4513409&units=${ unit }&appid=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`;
    const rawdata: any = await this.http.get<any>(url).toPromise();
    const data: TempAndIcon = {
      temp: rawdata.main.temp,
      icon: this.getIconUrl(rawdata.weather[0].icon),
      units: unit
    };
    this.setActionRunnerFn(this.actions.RECEIVED_WEATHER_DATA, data);
  };

  getIconUrl = (icon: string) => `http://openweathermap.org/img/wn/${ icon }@2x.png`;

}
Enter fullscreen mode Exit fullscreen mode

The initiateWeather function is a bit boring, other than the fact that is uses a function that's passed in from the Data Store Service (did this to avoid circular references).

The API call is also pretty straight forward, except where the code is set to use .toPromise() allowing for async/await to be used, the data cleaned up and passed to the data store as RECEIVED data.

Conclusions

Late last night, I had these visions of dots swimming in my head: the dots and lines used to describe actions and their relation to data in ReactJS data stores ... and the dots and lines used to describe data movement and management of Observables and subscriptions.

Pattern Pros

Having done all this (written the code and this article), I believe there is a certain cleanliness to what has been designed. There are certainly strengths as defined at the beginning of the article.

  1. It provides a Single Source of Truth for the application's data and states.
  2. It centralizes the process of triggering actions.
  3. It allows for a location for "global" functionality.
  4. It provides a central location for all components and services to see data and state changes.

Pattern Cons

At the same time, I generally use the Subject and BehaviorSubject inside the service where the data point is generated; a much simpler and leaner method ... bypassing the need for actions and a data store and their inherent weight of code to be developed and managed over time.

  1. It takes more time to setup and configure.
  2. Need to take into account use of the store by other services; there can be issues with circular dependencies without care.

Finally

I'm not sure I actually sprang from my bed, but I did get up very early as these 'dots' swirled around in my head ... I put this code and article together.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .