Understanding Reactive Programming Patterns in Modern JavaScript Frameworks


Introduction
Reactive programming has become a cornerstone of modern web development, especially in frameworks like Angular, React, and Vue. But what exactly makes code "reactive," and how do different reactive patterns serve different purposes? In this article, we'll break down the fundamental concepts of reactive programming using a simple yet powerful conceptual framework.
Whether you're a seasoned developer or just getting started with reactive programming, understanding these patterns will help you make better architectural decisions and write more maintainable code.
The Two Dimensions of Reactive Programming
Reactive programming can be understood through two key dimensions:
Cardinality: Are we dealing with a single value or multiple values?
Direction: Are we pulling data or is data being pushed to us?
This creates a matrix of four distinct patterns, each with its own use cases and characteristics:
Let's explore each quadrant in detail.
Promises: Single Value + Push
Promises represent asynchronous operations that will eventually complete with a single result (or an error).
// A promise that pushes a value when completed
function fetchUserData(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json());
}
// The result is pushed to us when ready
fetchUserData(123).then(user => {
console.log(user);
});
Key characteristics:
The operation is initiated immediately
The result is pushed to you when ready
A promise resolves exactly once
Once resolved, a promise cannot produce new values
Promises shine when dealing with asynchronous operations that produce a single result, like HTTP requests or file operations. However, they're not designed for scenarios where multiple values arrive over time.
Functions: Single Value + Pull
Functions represent the most familiar programming pattern for most developers. When you call a function, you're actively requesting (pulling) a single value.
// A simple function that returns a single value
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Pulling the value when we need it
const total = calculateTotal(cartItems);
Key characteristics:
You decide when to request the value
The function executes synchronously
Once the function returns, execution is complete
The result is immediately available
Functions are perfect for synchronous operations where you need a single result immediately. However, they fall short when dealing with asynchronous operations or streams of data.
Iterators: Multiple Values + Pull
Iterators allow you to work with collections of data by actively pulling values one at a time.
// Using an iterator to process multiple values
const numbers = [1, 2, 3, 4, 5];
const iterator = numbers[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
console.log(result.value);
result = iterator.next();
}
More commonly, we use abstractions like for...of
loops:
for (const number of numbers) {
console.log(number);
}
Key characteristics:
You control the pace of iteration
You decide when to request the next value
Values are typically processed synchronously
You can stop requesting values at any point
Iterators are excellent for processing collections of data where you want explicit control over the iteration process. They're less suitable for handling asynchronous data streams or events.
Observables: Multiple Values + Push
Observables represent streams of data that can push multiple values over time.
// An observable that pushes multiple values
import { fromEvent } from 'rxjs';
const clickObservable = fromEvent(document, 'click');
// Values are pushed to us as they occur
const subscription = clickObservable.subscribe(event => {
console.log('Click detected at:', event.clientX, event.clientY);
});
// Later, we can stop receiving values
setTimeout(() => subscription.unsubscribe(), 10000);
Key characteristics:
Values are pushed to you as they become available
Can emit multiple values over time
Can represent infinite streams (like user events)
Supports cancellation via unsubscribe
Observables excel at handling event streams, real-time data, and complex asynchronous workflows. They're the most powerful but also the most complex of the reactive patterns.
Show Image
Signals: A New Reactive Primitive
Angular's Signals represent a newer reactive primitive that blends aspects of both pull and push models:
// Angular Signal example
import { signal } from '@angular/core';
// Create a signal with initial value
const count = signal(0);
// You can "pull" the current value
console.log(count()); // 0
// You can "push" a new value
count.set(1);
// Or update based on previous value
count.update(value => value + 1);
// Components automatically re-render when signals change
Key characteristics:
Fine-grained reactivity
Both readable (pull) and writable (push)
Synchronous by default
Optimized for UI updates
Signals provide a more lightweight alternative to Observables for many UI-related reactive scenarios, especially when dealing with component state.
Choosing the Right Pattern
Each reactive pattern has its strengths and ideal use cases:
Pattern | Best for |
Functions | Simple, synchronous calculations |
Iterators | Processing finite collections with control |
Promises | Single asynchronous operations |
Observables | Event streams, complex async workflows |
Signals | UI state, component reactivity |
The key is selecting the right tool for the specific problem:
Need a single value right now? Use a function.
Need to process a collection at your own pace? Use an iterator.
Need a single value from an async operation? Use a promise.
Need to handle multiple values over time? Use an observable.
Need reactive UI state in Angular? Consider signals.
Practical Application in Angular
In Angular applications, you'll commonly use a mix of these patterns:
@Component({
selector: 'app-user-dashboard',
template: `
@if (userLoaded()) {
<h1>Welcome, {{ userName() }}</h1>
@for (notification of notifications$ | async; track notification.id) {
<div>{{ notification.message }}</div>
}
<button (click)="refresh()">Refresh</button>
}
`
})
export class UserDashboardComponent {
// Signal (pull-based reactivity)
userName = signal('');
userLoaded = signal(false);
// Observable (push-based stream)
notifications$ = this.notificationService.getNotifications();
constructor(
private userService: UserService,
private notificationService: NotificationService
) {
// Promise (single async value)
this.userService.getCurrentUser()
.then(user => {
this.userName.set(user.name);
this.userLoaded.set(true);
});
}
refresh() {
// Function (direct method call)
this.notificationService.refresh();
}
}
Conclusion
Understanding the different reactive programming patterns is essential for building robust, maintainable applications. By recognizing whether you need single or multiple values, and whether pulling or pushing is more appropriate, you can select the most suitable pattern for each situation.
As web applications grow more complex and real-time features become more common, mastering these reactive patterns will make you a more effective developer, regardless of which framework you use.
What's your experience with reactive programming? Which pattern do you find yourself using most often? Share your thoughts in the comments below!
If you found this guide helpful, leave a like ❤️ or share it!
Subscribe to my newsletter
Read articles from Koustubh Mishra directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Koustubh Mishra
Koustubh Mishra
👋 Hey, I’m Koustubh (Kos) 💼 Frontend Engineer | Angular & Node enthusiast in the making 🚀 Building beautiful UIs, exploring modern dev tools 📚 Always learning: Angular internals, Signals, React, AWS ✍️ Sharing dev insights on LinkedIn, especially around Angular & GenAI 🎨 Anime sketcher | 🏍️ Rider | 🎧 Music keeps me going | 🏋️♂️ Gym is my reset button