Manage Async Code Like a PRO With RxJS

Authors
Paweł Trysła
Software Engineer
@
Callstack

If you’ve ever struggled with managing async code or felt that Promises lacks essentials features like cancellation, better composition or in general are feature-poor, here comes the Saviour — Observables and RxJS.

Observable itself is a new type introduced by proposal-observable as a part of ECMAScript specification. Since this is a proposal, you can’t use it right now, without any Observable library.

In this article, we will focus on the most popular implementation of Observables — RxJS, so quick disclaimer:

Observables, operators and implementation details mentioned in this article apply to RxJS only.

You should treat Observable the same as any other type like Array or Promise. You can instantiate it, modify, pass and whatever you like.

You can also treat Observables as a Set of (usually async) events, meaning Observable can emit multiple values (events) over time. This is one of the most significant differences between Promises — once Promise resolves, it’s done, at this point it’s just a wrapper around the data. You can attach <rte-code>then<rte-code> to it afterwards, but you will always get the same value. The second huge difference is that Observables are lazy/cold, whereas Promises are eager/hot. It means that once Promise is created, it must either resolve with a value or throw an error. On the other hand, Observables won’t do anything unless you subscribe to them (we will talk about it later).

Despite the differences, Observables and Promises also share some similar features. One of them is notification channels. With Promises, you have 2 channels: resolve and error. You can add a handler function to resolve channel using <rte-code>then<rte-code>, which will get called when Promise resolves with a value, and error handler to error channel using <rte-code>catch<rte-code>.

Observables, also implements those 2 channels named next, error, but also have another one  — complete. Next and error channels are analogous to Promise’s resolve and error. Complete channel is fired only when Observable is done emitting values. You can think of Promise’s resolve channel as a combination of next and complete  —  Promise can’t resolve multiple times so, when you get value it also completes.

Creating Observable

Let’s see how we can create an Observable instance:

import Rx, { Observable } from 'rxjs/Rx';
const event$ = Observable.create((observer) => {
console.log('Starting counting...');
for (let i = 0; i < 5; i++) {
setTimeout(() => {
observer.next(i);
}, i * 1000);
}
});
view raw gistfile1.txt hosted with ❤ by GitHub

Simple Observable creation

In this example, we use <rte-code>Observable.create<rte-code> factory provided by RxJS, which accepts a function. From within, we have access to <rte-code>observer<rte-code> object, on which we call <rte-code>next<rte-code> method passing values to be emitted by the Observable. In this case, we pass numbers from <rte-code>0<rte-code> to <rte-code>4<rte-code>, each one within 1s interval.

$ is a common postfix to distinct a stream of data/events.

As I mention earlier, Observables are cold, thus you won’t see <rte-code>console.log<rte-code> being called, if you try to run this example.

Transformation and composition

RxJS gives you an extremely powerful toolset to transform or compose your Observables. Those tools are called operators. Remember, when you wanted to change every element of an array? You would use Array#map method. You can do the same with Observables!

const updatedEvent$ = event$
.filter(value => value % 2 === 0)
.map(value => `${value} is even`);
view raw gistfile1.txt hosted with ❤ by GitHub

Simple filter and map operations

The code above will first check if each emitted value from <rte-code>event$<rte-code> fulfils the function we passed to <rte-code>filter<rte-code> operator. If so, it will map this value to a string. Every value, which doesn’t fulfil <rte-code>filter<rte-code>’s function will be discard. This highlights another useful feature — output Observable doesn’t have to emit the same amount of values as the input one does. In other words:

Input/output ratio of emitted values doesn’t have to be 1.

Note that we assigned results of those operations to a new variable. It’s because, each operator returns a new Observable — all operators are pure, same as <rte-code>Array#map<rte-code> and <rte-code>Array#filter<rte-code>.

Subscription

Up this point our Observable won’t do anything, since we haven’t yet subscribed to it. Let’s do it now:

const subscription = updatedEvent$.subscribe(value => {
console.log(value);
});
view raw gistfile1.txt hosted with ❤ by GitHub

Simple subscription with next handler only

As you can see, we need to call <rte-code>subscribe<rte-code> method on our Observable. It’ll make it hot, thus it will start emitting values. In console you will see:

0 is even
2 is even
4 is even
view raw gistfile1.txt hosted with ❤ by GitHub

<rte-code>subscribe<rte-code> method also returns a subscription object, which you can use to unsubscribe (stop observable execution or free resources) by calling <rte-code>subscription.unsubscribe()<rte-code>.

In this example, we’ve only passed a single function to subscribemethod, however it’s worth knowing, that you can also pass error handler and complete handler:

const subscription = observable.subscribe(
value => console.log(`Observer got a next value: ${value}`),
error => console.error(`Observer got an error: ${error}`),
() => console.log('Observer got a complete notification')
);
view raw gistfile1.txt hosted with ❤ by GitHub

Next, error and complete handlers

What exactly are the operators?

Operators are nothing more than a pure functions, which take input/current Observable and return a new Observable with applied modifications thus they are chainable. Each operator can belong to 1 category out of 9. The most commonly used are:

  • creation operators
  • transformation operators
  • filtering operators
  • combination operators
  • error handling operators

Each operator regardless of it’s category can be:

  • instance operator — uses Observable instance to which they’re bound, in other words, they use <rte-code>this<rte-code> keyword to refer to input Observable
  • static operators — aren’t bound to any specific Observable instance, instead of using <rte-code>this<rte-code>, they use first argument as an input Observable

Almost all creation operators are static, whereas the rest are instance operators. However, some of them such as <rte-code>concat<rte-code> or <rte-code>merge<rte-code> have both instance and static versions.

RxJS gives you ~120 operators to use, which is an overwhelming amount, but don’t worry most of them are just more specific variants of another or are used in very specific cases. Take for example <rte-code>map<rte-code> operator, if you know what it does, you also know what <rte-code>mapTo<rte-code> does — same as <rte-code>map<rte-code>, it projects value to another, but instead of accepting a function to get the new value, it uses the same single one every time.

Here’s a list of the most common operators, that you should get familiar with before diving deeper:

  • do
  • mergeMap
  • switchMap
  • map
  • concat
  • catch
  • scan
  • filter

If you ever stuck, wondering which operator to use, here is a very useful Q&A based guide to help you find the exact one.

Error handling and chains isolation

Let’s talk a bit more about error handling. Usually, while developing your app, you have to deal with 3rd party modules or need to fetch some data from remote server. Regardless of what you need to accomplish, the code can always throw an error, which will cause an Observable to close itself and run some teardown logic, thus no events will be able to go through it! Once the error is throw, the Observable is done aka dead.

Sometimes, this kind of behaviour is desired. In other cases, you need to handle errors properly. So how do you do that? Let’s first look at the example:

import { ajax } from 'rxjs/observable/dom/ajax';
const API_URL = 'http://some-domain.ext';
const userInput$ = getStreamOfUserInputEvents();
const subscription = userInput$
.filter(evn => evn.type === 'buttonClicked')
.mergeMap(evn => ajax.getJSON(`${API_URL}/do-something`))
.catch(error => {
// handle error
console.error(error);
return Observable.of('ajaxFailed');
})
.subscribe(subscribeToUserInputEventsStream);
view raw gistfile1.txt hosted with ❤ by GitHub

Naive implementation of error handling

As we can see, this Observable should make an Ajax call on specific event — buttonClicked. If the request fails, the thrown error will be handled by catch operator. Even though, the error’ed Observable (userInput$) is replaced by a new Observable.of('ajaxFailed'), the error will still cause it to close. No events will go through it anymore.

To fix this problem, we need chains isolation:

import { ajax } from 'rxjs/observable/dom/ajax';
const API_URL = 'http://some-domain.ext';
const userInput$ = getStreamOfUserInputEvents();
const subscription = userInput$
.filter(evn => evn.type === 'buttonClicked')
.mergeMap(evn =>
ajax.getJSON(`${API_URL}/do-something`)
.catch(error => {
// handle error
console.error(error);
return Observable.of('ajaxFailed');
})
)
.subscribe(subscribeToUserInputEventsStream);
view raw gistfile1.txt hosted with ❤ by GitHub

Proper implementation of error handling

We just moved <rte-code>catch<rte-code> into the inner chain created by <rte-code>ajax.getJSON<rte-code>. Now, if the error is raised, we replace it with a new Observable (in the inner chain), so in every case, the final output of the inner Observable chain will be a non-error’ed Observable.

Most of the time, you don’t want your Observable to die, so just isolate the error-prone chain from the main one, and add <rte-code>catch<rte-code> to it.

Observables can be also synchronous

Although, Observables are extremely useful at managing asynchronous code, like WebSockets, events or user input, they can also be beneficial for synchronous tasks. Suppose, you have some big data set like an array. Observables might come in handy, where you need multiple <rte-code>Array#filter<rte-code> and <rte-code>Array#map<rte-code> operations to be chained together.

Consider the following example:

const input = [];
for (let i = 0; i < 100000; i++) {
input.push(i);
}
const output = input
.filter(element => element % 2 === 0)
.map(element => element * 2);
console.log(output);
const subscription = Observable.from(input)
.filter(element => element % 2 === 0)
.map(element => element * 2)
.reduce((accumulator, current) => accumulator.concat(current), [])
.subscribe(output => {
console.log(output);
});
view raw gistfile1.txt hosted with ❤ by GitHub

Modify large data set using array methods vs RxJS operators

Both variants will produce the same result, however Observables will be more performant and won’t create intermediate arrays. It’s due to the fact that, all RxJS operators will be applied in single iteration, whereas Array implementation will go through whole array to filter elements, then again over the intermediate array created by Array#filter method.

What’s next?

In case, you want to broaden your knowledge about RxJS and Observables, I strongly suggest to watch RxJS 5 Thinking Reactively and Advanced RxJS: State Management and Animations, both done by Ben Lesh or to read a RxJS documentation.

In the next article, we will talk how to use RxJS to manage side effects in Redux app.

Insights

Learn more about JavaScript

cover

Jun 26Article

React Native Legal: CLI & Programmatic API - Unified Tooling for License Attributions

Discover React Native Legal, the first unified solution for managing open-source license attributions. Automatically handle both JS & native dependencies in any project via a powerful CLI and programmatic API.

cover

Dec 14Article

ReasonML for JS developers

If you are a JavaScript developer and want to learn a new functional language, this article is a must-read for you. Check it out!

cover

Dec 12Article

JavaScript Hacking 101

In this article we takie a look at how things work in JavaScript under the hood.

cover

Aug 1Article

ECMAScript ES8 and Beyond

Quick introduction to ECMAScript proposals ES8 with the list of currently available presets and presentation of some features of each preset.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.