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:
- It provides a Single Source of Truth for the application's data and states.
- It centralizes the process of triggering actions, giving a clean accounting of what's happening (one
console.log
to show them all). - It allows for a location for "global" functionality, such as a spinner when an API request is in-flight.
- 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'
};
}
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:
- Changing the weather unit (Imperial (F) or Metric (C)).
- Initiate, trigger, and receive weather data (initiate sets up a one minute
setInterval
so that the data trigger fires over and over). - 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;
}
};
}
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';
});
}
The actual "toggle" is as follows ...
toggleFavicon = () => {
this.dataStore.processAction(this.actions.TOGGLE_ICON, this.iconState);
};
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);
};
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;
});
}
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>
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;
});
}
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`;
}
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.
- It provides a Single Source of Truth for the application's data and states.
- It centralizes the process of triggering actions.
- It allows for a location for "global" functionality.
- 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.
- It takes more time to setup and configure.
- 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.
Top comments (8)
As for the pattern, it's just overengineering.
Simple and basic RxJS provides everything you need with much less code (StackBlitz Example).
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.
With basic RxJS you do not even need actions, you just call service methods.
So can every
providedIn: 'root'
servicePutting everything in one class is not a central location and it will quickly become hard to use.
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.
I’m glad you enjoyed the article!
it seems like ngrx or ngxs?
My thought as well.
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!
You mean Redux/
useReducer
, right? There is no such thing as "ReactJS-like data store"Sure!