Observables, Side-Effects, and Subscriptions: Some Reactive Best Practices
blog.eyas.sh
blog.eyas.sh
While I think they are a powerful tool, I find that a lot of developers get them confused as a drop in replacement for promises and do things like nest subscriptions etc. For most standard CRUD apps, either do as the author suggests, and use an async pipe in your template, or, convert them to promises as soon as possible.
What are they good for? Streams of data. Web Sockets, iframe post messages, event handlers for instance. Anything that is actually real time and makes changes without deliberate action.
I guess the key is to minimise the amount of code that directly subscribes, don’t mutate data as it flows through an observable, and where possible, prefer deriving one observable from another over using imperative code to push data from one observable into another.
I can map, filter, etc. my app's actions to perform any side-effect I want, possibly sending a success or failure action on completion. I can even listen for changes in the store or other streams to dispatch actions.
You can still do everything you need with Thunk, Sagas, etc., but I enjoy the ease and flexibility that RxJS gives me.
Libraries that help with this pattern:
Redux Observables: https://redux-observable.js.org/
NGRX Effects: https://ngrx.io/guide/effects
We are using reactive programming basically everywhere. Example (Angular):
The Angular Router has an Observable "paramMap". We don't subscribe to it but are creating another Observable for the data that should be loaded.
getData$ = (
paramMap$: Observable<ParamMap>
): Observable<MyData> => {
return paramMap$.pipe(
// Create a query string
map(paramMap => {
// some pure function - business logic
return getSomeQueryBasedOnTheRouteParams(
paramMap
);
}),
// get the data
// select$ is a method on the customStore that wraps the http client and does some more stuff
// The switchMap is actually one of the concepts that I found initially not the easiest to understand
switchMap(someQuery=> {
return this.customStore.select$({
query: someQuery
});
}),
map(rawData => {
// pure function with business logic
return modifyData(rawData);
}),
shareReplay()
);
};
Then it goes on with filtering, etc. getFilteredData$ = (
// filter:$ is in our case a simple Subject we're calling next(newFilterValue)
// NGRX, NGXS or Akita are popular state management libraries to manage this kind of state
filter$: BehaviourSubject<string>,
data$: Observable<MyData>): Observable<MyData> => {
combineLatest(filter$, data$).pipe(
// Do the filtering using pure functions
map( ...
return myFilterFuntion(filter, data);
)
)
};
The filteredData$ Observable can be consumed via async pipe in a component.The business logic is the same as in imperative-pull code. But using the abstraction of RxJS a lot of boilerplate and indirection vanishes. RxJS is not the easiest abstraction to learn (for me), but as with all, once you're comfortable a lot of the likely intimidating code above will become familiar. You just focus on writing business-logic in pure functions and combine it with RxJS.
It encourages the use of IIFEs as a way to declaratively isolate parallel async operations from serial async operations. Also it shifts control to the data consumer instead of the producer; this should reduce the likelihood of memory leaks.
Hmmmm, I wonder how a subscription can cause side effects? I can only think of mutating the data in the subscription. But that should not be done.
We are using RxJS with great success but you should go all the way with Observables: All computations/combination/etc. should be done in pipe()s. If you need to combine some plain data with Observables create a subject for the plain data and pipe(combineLatest(),map(), ...). Don't mix it into subscriptions. Subscribe at the very end of the chain. No modification in subscribtions.
A subscription is a side effect.
That’s where the vast majority of tutorials, blog posts and presentations end. And then you’re stuck with trying to implement a simple login flow, or reading data from a file, and... nothing really works, everything is overengineered and impossible to debug and trace.
In his book the author of RxJava openly admits it took him several months to grok reactive streams. And he was taught by the author of reactive extensions for dotnet.
Programming with reactive stuff is essentially async programming (never easy) where your data is handled, transformed in async blackboxes (hardly any implementation of Rx has a good way to observe, debug and trace data through them), and is delivered to sinks/subscribers in async manner.
And yet basically everything you find on the topic will cheerfully tell you how easy this stuff is, and present you with a toy code that fits on a screen.
https://twitter.com/git_commit_m/status/1014915134949429248
There is a pretty huge documentation problem with observables.
* How do you end the observable's subscription when any of X, Y or Z happen (and at least one of these is a timer)?
* How do you emit N network requests in series, without nesting subscriptions? (ideally you won't need to, but not all APIs are well behaved)
* How do you transform a node-like stream to an observable?
This is just from last week, and some I'm able to do from scratch better than others.
TESTING them is challenging as well. There are non-obvious failures when you use the typical unit testing patterns (a testing framework will report that function was/wasn't called and you're not sure why, because per the test setup that shouldn't be what happened). Almost two years in and I have no idea what the marbles mean, but soon I'll start trying to grok them
But there's no way I see how I can do my job without them. The applications I work on take updates from 1) keyboard 2) mouse 3) direct websocket subscriptions 4) indirect websocket subscriptions 5) xhr calls 6) other messaging patterns that aren't as common.
So I'm at the point where I can easily tell you the differences between switchMap, concatMap, and mergeMap (of which flatMap is an alias), easily pull out filter, map, combineLatest, tap, etc. Sometimes when I'm in the weeds it does help to think that they're just functions, but at no point would I dare tell anyone that this stuff is easy. It's easily the hardest thing I've learned since I started my current job.
1. Observables are best when used as Behaviours from classical FRP formulation by C. Elliot. It's a value that's changing over time, and you should only compose it (map, combineLatest, switchMap, merge ...) and push subscriptions to the edges - for example asyncPipe in angular. That makes writing "reactive" (as in Excel cells) code a breeze. Reacting to changes as events (like button click) should be handled with care, with first/takeWhile and so on. But simple cases are still nice looking.
2. Observables are bad as framework for coordination problems. Complex flows of events can easy get out of hand, and suddenly you are passing objects with values and some state through streams or nest switchMaps instead of piping them. Often such code could be written with async/await and would be ten times cleaner. So any code which dealt with changes as events and had branches/state because of that, we rewrote with async/await. Examples are POST/PUT/DELETE requests with logic around authentication/authorization/timeouts. You could model that as stream but when you access closure from outer switchMap it's starts being unwieldy.
Sadly, issues like this seem to crop up all the time when using reactive programming. I'm not as anti-reactive as most other commenters here seem to be, but there's no doubt that certain areas, particularly memory safety, sharing and buffering, seem hard to understand and get right.
Probably worth fixing that though, and creating those resources out there.
Doing this reduces risk of side effects and help reading the code/debugging.
Also you must take in account stuff like back-pressure and multicasting observables, which are as useful as hard to master and maintain correctly.
Really once you stop putting subscriptions in subscriptions by using the switchMap or switchMapTo operators, a lot of errors start to become clear and avoided.
https://medium.com/@rakia/promise-vs-observable-vs-stream-16...
The big difference is the pull-vs-push scheme, making Observables lazy-by-default, and just subscribed vs unsubscribed to.
Observable is a theoretical concept in reactive programming, popularized by ReactiveX author Eric Meijer.
It's gained quite a bit of popularity because ReactiveX is heavily used in Java frontends and JavaScript (Angular especially).
There are many others, and then the differences go away.
In fact, Rx can be traced directly to the synchronous dataflow[1] of Lustre[2] and Esterel[3], which in turn goes back to Lucid[4], "The Dataflow Programming Language"[5].
It took a bit to ferret this out of the publication record, but when I asked Erik he confirmed.
[1] https://ptolemy.berkeley.edu/publications/papers/87/synchdat...
[2] https://en.wikipedia.org/wiki/Lustre_(programming_language)
[3] https://en.wikipedia.org/wiki/Esterel
[4] https://en.wikipedia.org/wiki/Lucid_(programming_language)
[5] http://www.cse.unsw.edu.au/~plaice/archive/WWW/1985/B-AP85-L...
But sure enough, if all we care about is a "collection of observed values, asynchronously" 'Stream' and 'Observable' fit the bill in all(most?) implementations.
It’s one of those really nice pieces of code that’s just small enough to get your head around but at the same time totally real and useful.