Next in this series of posts we take a look at what I think will likely be the thing that people struggle to get their head around the most (I know I have) and that's RxJS.
In principal and concept, it's a fairly simple idea, but in implementation it can often get a little confusing.
Nevertheless, I'll do my best to explain the basics.
About RxJS
Taken from the RxJS docs, we can describe RxJS as follows:
RxJS is a library for composing asynchronous and event-based programs by using observable sequences.
Boiling this down, I think the keyword in this description is "observable" as what RxJS does is allow us to work with data and collections of data and have components subscribe to those data structures and be notified when they change. This allows us to create reactive user interfaces which update as the data changes, rather than having potential out of sync views or the need to poll for changes.
The key concepts that we'll cover of RxJS are:
- Observables
- Subscriptions
- Subjects
- Operators
Observables
You can kind of think of Observables as similar to Promises, however Observables can resolve their value multiple times over its lifetime, where as a promise will only resolve once.
Lets take a look at a basic example
import { Observable } from 'rxjs';
const observable = new Observable(function subscribe(subscriber) {
const id = setInterval(() => {
subscriber.next('hi');
}, 1000);
});
Here we define an observable and provide it a function to execute for each subscriber to this observable. Within that function we can return a value at anytime using subscriber.next(value)
. This could be either syncronously or as per our example due to our use of setInterval
, asynchronously, and all observers to our observable will be notified.
So what does our observable do? For all subscribers it will simply return the string hi
every second.
Subscriptions
Observables are pretty useless though unless there is something to subscribe to those changes and so in order to receive updates to an observable we must first subscribe to it.
observable.subscribe((x) => console.log(x));
If we were to now run our example we would see the word hi
logged to the console every second.
Subjects
Observable objects in and of themselves are just that, observable, but more often than not we are going to want to be able to modify the content we observe and for those changes to be notified to all subscribers.
For this we can use Subjects.
With Subjects we can make changes to our observed value and have that change pushed out to it's subscribers.
import { Subject } from "rxjs";
// Create the subject
let value$ = new Subject<string>();
// Subscribe to the subject
value$.subscribe(x => console.log(x));
// Update the subject
value$.next("Hello world");
// Logs:
// "Hello world"
Speciality Subjects
As well as the basic Subject
type, there are a couple more speciality subject types that you may want to be familiar with / that you may see example of in the backoffice codebase (there are more than I present here, but I think these will be the most common).
-
BehaviourSubject - A subject that has the concept of a current value that you can access via
subject.getValue()
- ReplaySubject - A subject that will replay it's change history for any new subscribers.
Operators
So far I don't think anything we've seen has been that scary, but I think where people are likely to get a little lost is with the next subject of operators, but hopefully we can show it's not that complex a concept.
So far in our examples we've only returned a simple string value, but lets say we have an observable collection of entities for something we are interested.
import { from } from 'rxjs';
let people = from([
{ name: 'Matt', age: 42 },
{ name: 'Mark', age: 38 }
]);
Now lets say that as an observer your only interested in people younger than 40 and you only need the peoples names. We can achieve this using pipeable operators like so.
people.pipe(
filter(x => x.age < 40),
map(p => p.name)
)
.subscribe(names => console.log(x))
// Logs:
// "Mark"
The operators in this example are the filter
and map
methods. These perform some kind of action and return the values we are interested in. The pipe
method is just a convenience method to help us pipe the method calls together, such that the output of the first opperator, feeds into the next opperator. Without the pipe
method we'd instead end up needing to call our operators like map(filter(items, x => x.age < 40), x => x.name))
so you can see how that could get messy.
There are a lot of operators available in RxJS and many of them are similar to standard array operators such as the filter
and map
operators above. I'd take a look at the Learn RxJS site for a complete list of the avilable operators.
Even with the "simplification" of using the pipe method though, I do think this whole area can be pretty confusing. I think the use of quite generic opperator terms can make it hard to follow what's going on, especially if you have multiple opperator chained together.
RxJS in Umbraco
The new backoffice mostly uses RxJS for reactive services, ie services that fetch entities that you want all subscribers to be notified of changes. These are usually imlpemented using a BehaviourSubject
that holds an array of the entities the service manages and then this is exposed as an observable that can be filtered on.
One main issue I have around this currently is because the thing being observed is a single value that is an array, it can make filtering the observable pretty ugly as you'll often see code this like this:
_someService.getEntities()
.pipe(map(x => x.filter(y => y.alias == 'something') ))
.subscribe(...);
Notice here how we have pipe(map(
and in the map function we have a filter. The issue here is because the observable isn't a stream of individual values, rather it's a single value that itself is an array, you end up always using pipe(map(
to re-map the structure of the inner array value, and inside the map
use standard array operators to do the filtering. This to me just adds more weirdness to this already confusing API.
Personally I think it would be worth Umbraco introducing an abstraction here to hide away this pipe(map(
mess.
Conclusion
I'll be honest, I've really struggled to finish this blog post and I'm very aware that I've missed even some basic concepts, but I've done so because I think the above is the minimum snapshot to understand to be able to navigate the new backoffice code without getting overloaded with too much information.
I'd really suggest you do read up on RxJS fully to get to grips with more of the finer details.
I think the reason I've struggled to finish this post is that I'm just not confident that a) I fully understand things myself or b) whether Umbraco are using it in a best practice way themselves.
Feel free to comment if I have missunderstood anything / you have a better way of explaining these concepts.
I think the goal of making services reactive so that they broadcast updates to models that may be being accessed in multiple places is a nice goal as it ensures the UI stays in sync, but I'm yet to fully decide if RxJS is the best approach, or if it is, can it be abstracted a little more to make the API a little nicer.
Top comments (0)