DEV Community

Cover image for Creating a Data Store in Angular
bob.js
bob.js

Posted on

Creating a Data Store in Angular

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.

Discussion (8)

Collapse
evgeniypp profile image
Eugene Petryankin

As for the pattern, it's just overengineering.

Simple and basic RxJS provides everything you need with much less code (StackBlitz Example).

It provides a Single Source of Truth for the application's data and states.

You do not have Single Source of Truth, because it does not mean that you have one class to handle the data, it means you have one immutable object with all the actual data in it.

It centralizes the process of triggering actions.

With basic RxJS you do not even need actions, you just call service methods.

It allows for a location for "global" functionality.

So can every providedIn: 'root' service

It provides a central location for all components and services to see data and state changes.

Putting everything in one class is not a central location and it will quickly become hard to use.

Collapse
rfornal profile image
bob.js Author

FYI … pretty sure I explained it’s an exercise. Think a of this as an academic exercise in what “could” be done. For me, it provided a better understanding of all the tooling involved and pulled together some ideas in a way that made some concepts clearer.

Collapse
rfornal profile image
bob.js Author

I’m glad you enjoyed the article!

Collapse
maxisam_tw profile image
Sam Lin

it seems like ngrx or ngxs?

Collapse
fanchgadjo profile image
Francois K

My thought as well.

Collapse
rfornal profile image
bob.js Author

True. However, the takeaway I was aiming for was the cleaner use of observables. The use of Stores was a means to an end, not the goal. Thanks for the comment!

Collapse
evgeniypp profile image
Eugene Petryankin

ReactJS-like data store

You mean Redux/useReducer, right? There is no such thing as "ReactJS-like data store"

Collapse
rfornal profile image
bob.js Author

Sure!