Using BehaviorSubject to Handle Asynchronous Loading in Ionic
It is common in Ionic applications (and Angular applications more broadly) to create some kind of service to handle loading data from some source. This might be from a local JSON file, local storage, a remote database, or some other kind of data store. The idea is that the service handles any of the complexities required to fetch the data, and from anywhere else in your application you can just make a call to the service to retrieve the data.
However you are storing your data, the process for retrieving that data will almost always be asynchronous. If you are not familiar with the concept of asynchronous and synchronous code, I would recommend reading: Dealing with Asynchronous Code in Ionic.
Let's take a look at a quick example:
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage-angular';
@Injectable({
providedIn: 'root',
})
export class DataService {
private _storage: Storage | null = null;
constructor(private storage: Storage) {
this.init();
}
async init() {
const storage = await this.storage.create();
this._storage = storage;
}
getData() {
return this._storage.get('someData');
}
}
In this case, the getData
method returns a promise and we could make a call to that function to retrieve the data we want:
const data = await this.myDataService.getData();
We are just using the Ionic Storage API as an example which returns a promise, but you could substitute other methods for loading data here as well (e.g. an HTTP request to a server which would return an observable). The example above is fine, and it will work, but every time you want to use the data in your application the DataService
is going to launch another request to retrieve a fresh copy of that data. That's not really much of an issue when dealing with small amounts of data in local storage, but if you need to hit some server endpoint it's not really ideal to keep hitting the server with unnecessary requests.
As an alternative to launching a request to retrieve the data every time, we might instead decide to just load it once and store it on a member variable in the service. That might look something like this:
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage-angular';
@Injectable({
providedIn: 'root'
})
export class DataService {
private _storage: Storage | null = null;
public myData: string;
constructor(private storage: Storage) {
this.init();
}
async init(){
const storage = await this.storage.create();
this._storage = storage;
}
async load(){
this.myData = await this._storage.get('someData');
}
}
Now rather than having to request the data from storage every time, we just have to trigger this once by calling load()
in the data service. Once the data has been loaded, we can grab it anywhere in the application like this:
this.myDataService.myData;
…but this has its downsides as well. First of all, if we try to reference myData
in the data service, there isn't really any way for us to know if the data has finished loading or not. If we try to access myData
before the promise in the load()
method has resolved then we are going to get a value of undefined
. This approach can work if you are certain that you will be accessing the data after it has loaded, or if for whatever reason it isn't that important if you access the data before it loads, but it isn't the most robust design and can lead to bugs/errors/issues.
Another downside of this approach is that is doesn't handle changing data very well – if the data changes then whatever parts of your application accessed the data won't know that it has changed.
Outline
Using a BehaviorSubject
With that long-winded introduction out of the way, let me introduce you to the concept of a BehaviourSubject
. Given the tone of this article so far, you can probably already guess that a BehaviorSubject
is just the thing we need to solve the issues I've mentioned above.
A BehaviorSubject
is a type of observable (i.e. a stream of data that we can subscribe to like the observable returned from HTTP requests in Angular). I say a type of observable because it is a little different to a standard observable. We subscribe to a BehaviourSubject
just like we would a normal observable, but the benefit of a BehaviourSubject
for our purposes is that:
- It will always return a value, even if no data has been emitted from its stream yet
- When you subscribe to it, it will immediately return the last value that was emitted immediately (or the initial value if no data has been emitted yet)
We are going to use the BehaviorSubject
to hold the data that we want to access throughout the application. This will allow us to:
- Just load data once, or only when we need to
- Ensure that some valid value is always supplied to whatever is using it (even if a load has not finished yet)
- Instantly notify anything that is subscribed to the
BehaviorSubject
when the data changes
It is much easier to see how this works if we just take a look at some code:
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { Storage } from '@ionic/storage-angular';
import { SomeType } from '../interfaces/movie-category';
@Injectable({
providedIn: 'root',
})
export class DataService {
private myData: BehaviorSubject<SomeType[]> = new BehaviorSubject<SomeType[]>(
[]
);
private _storage: Storage | null = null;
constructor(private storage: Storage) {}
async init() {
const storage = await this.storage.create();
this._storage = storage;
}
async load(): void {
await this.init();
const data = await this._storage.get('myData');
this.myData.next(data);
}
updateData(data): void {
this._storage.set('myData', data);
this.myData.next(data);
}
getData(): Observable<SomeType[]> {
return this.myData;
}
}
First, we initialise our BehaviorSubject
like this:
private myData: BehaviorSubject<SomeType[]> = new BehaviorSubject<SomeType[]>([]);
This looks pretty wacky, but if we remove the type information (i.e. in this example the BehaviourSubject
is returning an array of data of the type SomeType
) it becomes a little clearer:
private myData = new BehaviorSubject([]);
We are creating a member variable that is a new instance of BehaviorSubject
and we supply an initial value of []
to it (an empty array). Since it has an initial value, if we were to subscribe to this from somewhere:
this.myDataService.getData().subscribe((data) => {
console.log(data);
});
we would see an empty array logged to the console.
NOTE: Instead of accessing myData
directly by exposing it publicly, we instead create a method called getData
that returns the BehaviorSubject
by first casting it to the Observable
type. Generally, we want to make sure we are only calling next
on our BehaviorSubject
from one place (this service) so we don't want to give out the subject to other parts of the application which could then call its next
method. By casting it to an Observable
first it will cause type issues if someone tries to call its .next
method from outside of this service (you might also commonly see people returning the BehaviorSubject
using the asObservable
method if TypeScript isn't being used).
However, anytime we want to update that data we just need to call the next
method on the BehaviorSubject
:
this.myData.next(data);
This will cause the BehaviorSubject
to emit the new value, and anything that is subscribed to it will be instantly notified. Let's say that we were relying on this to populate a list in Ionic:
ngOnInit(){
this.subscription = this.myDataService.getData().subscribe((data) => {
this.myListData = data;
});
}
Initially, the list will be empty because it will just receive an empty array, but as soon as the load
method completes myListData
will be instantly updated with the new data. Likewise, if we ever update the data in the service in the future, we can just call the next
method again to supply anything that is subscribed to the BehaviorSubject
with the new data instantly.
Summary
A BehaviorSubject
is basically just a standard observable, except that it will always return a value. With the method of loading data using a BehaviorSubject
that we have discussed in this article, we can:
- Access the data without worrying about timing, because we know that we will always receive a valid value (even if it is just the initial value)
- Keep data updated automatically across the application
- Remove unnecessary requests to load data just for the sake of ensuring that the data is in fact loaded
Once you get your head around using Behavior Subjects, you will probably wonder how you ever survived without using them.