Reactive programming: writing loosely coupled software in web
By: Juan Carlos Arocha
Tags: JavaScript, Reactive Programming, Web Development

Photo by Joao Branco on Unsplash

In software development, there are two key concepts when designing a maintainable architecture: coupling and cohesion. We should always aim for loosely coupled and highly cohesive software, and although it’s possible to achieve this in paper, to express it in the implementation could sometimes be difficult — if not impossible — if we don’t choose the right tools and solutions available for the language/environment we are using.

In this article, we will see why is reactive programming such a good solution to decouple software in web and how to achieve it properly.

Why reactive programming?

First, why is reactive programming a good fit for web? Take a look at the following statements:

“When the user is successfully authenticated, then fetch the account details”

“When the user buys a product, send the data to the analytics tool”

“When the user account information is fetched and the location settings are available, then redirect the user to their preferred city”

If you have worked on relatively complex web projects, you should be familiar with those statements. This is because a website business logic is event-driven and with events comes orchestration — “when event A happens, then execute logic B”.

Reactive programming is a declarative programming paradigm that uses data streams for communication and change propagation. In other words, there’s a data flow that receives relevant information — e.g. “event A happened”— and there’s a set of subscribers that listen to this information and react accordingly — e.g. “execute logic B”. Sounds familiar?

This solution is a natural fit to solve event-orchestration problems and it does so bringing benefits that can make your architecture extremely easy to maintain:

Declarative code: event emissions + subscriptions — easy to understand and to follow, not only facilitating general maintenance, but also allowing to spot code smells without effort, e.g. emitting “redirect to preferred city” event in the user account information store logic — not very cohesive, is it? Decoupled code: minimal dependencies between different software components as they need only to know the available events so they can subscribe to them. No need to know implementation details like under which circumstances they are emitted or how exactly

How to do reactive programming in web (properly)

Although JavaScript is event-driven, it doesn’t mean reactive programming is built-in in the language — It requires a change of mindset and some basic setup. If you want to achieve it, you can follow these recommendations:

#1 Find a technically reliable global data stream that you can subscribe to

JavaScript provides many ways to do this, among which we can mention:

VanillaJS using addEventListener, dispatchEvent and CustomEvent RxJS library Angular + NgRx Store action stream Vue + Vuex action stream React + Redux action stream

#2 Consider all logic as a potential event for the stream

As JS is event-driven, all logic runs inside a handler of an existing event — even top-level code is executed on window.onload (or when the file is downloaded and executed).

This means that any logic can either be part of the current handler or constitute a new event with its own handler. For example, the following use-case:

When the user is successfully authenticated, then fetch the account details

Can be implemented all in the same handler:

async onAuthenticationFormSubmit(credentials) {

const session = await authenticateUser(credentials)

storeSession(session)



const accountDetails = await fetchAccountDetails(session.userId)

storeAccountDetails(accountDetails)

}

Or in separated handlers:

async onAuthenticationFormSubmit(credentials) {

const session = await authenticateUser(credentials)

storeSession(session) dispatchEvent(

new CustomEvent('onUserAuthenticated', { details: session })

)

} async onUserAuthenticated({ details }) {

const { userId } = details.session

const accountDetails = await fetchAccountDetails(userId)

storeAccountDetails(accountDetails)

}

When designing the workflow, consider all actions as a potential event and abstract it if it makes sense — don’t abuse because then you can get a lot of noise in the stream. It may seem overkilling, but doing this will help you:

To change your mindset from synchronous imperative programming, to asynchronous reactive programming and To populate the data stream with relevant events, making it reliable

#3 Never dispatch events that are not cohesive with the current handler

Following the same example, this is a big code smell:

async onAuthenticationFormSubmit(credentials) {

const session = await authenticateUser(credentials)

storeSession(session) dispatchEvent(new CustomEvent('onFetchAccountDetails', {

details: session.userId

}) dispatchEvent(new CustomEvent('onSendAuthAnalyticsData', {

details: session

}) dispatchEvent(new CustomEvent('onUpdateUserLocation', {

details: session.userId

})

}

Why? Because “fetching account details”, “sending analytics data” and “updating user location” have nothing to do with user authentication and this reference creates an unnecessary coupling, encourages dispatching more unrelated events (creating even more coupled code) and makes simple logics very hard to follow.

This is the recommended approach:

async onAuthenticationFormSubmit(credentials) {

const session = await authenticateUser(credentials)

storeSession(session)



dispatchEvent(new CustomEvent('onUserAuthenticated', {

details: session

})

} window.onload = () => {

addEventListener('onUserAuthenticated', fetchAccountDetails)

addEventListener('onUserAuthenticated', sendAuthAnalytics)

addEventListener('onUserAuthenticated', updateUserLocation)

} async fetchAccountDetails(event) { ... }

async sendAuthAnalytics(event) { ... }

async updateUserLocation(event) { ... }

#4 Expose the events to consumers as a contract

Think of the events you expose as a public API contract so you take them seriously when making changes — providing backward compatibility for example.

Also, it’s useful to have an “official” source of events, as simple as a list of string constants:

export const ON_USER_AUTHENTICATED = 'ON_USER_AUTHENTICATED'

export const ON_USER_SIGNED_OUT = 'ON_USER_SIGNED_OUT'

...

Summary

To write loosely coupled software is not an easy task and we need to choose the right solutions and tools so we can express our design in the implementation. In the case of web development, reactive programming is a natural and straight-forward way to achieve it.

The cost of setting up the data stream is almost zero as JavaScript is event-driven and there are plenty libraries to support this even further, so it’s highly recommended to use this paradigm as the default way of programming in web.