Understanding Reactive Programming Patterns in Modern JavaScript Frameworks

Koustubh MishraKoustubh Mishra
5 min read

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:

  1. Cardinality: Are we dealing with a single value or multiple values?

  2. 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:

PatternBest for
FunctionsSimple, synchronous calculations
IteratorsProcessing finite collections with control
PromisesSingle asynchronous operations
ObservablesEvent streams, complex async workflows
SignalsUI state, component reactivity

The key is selecting the right tool for the specific problem:

  1. Need a single value right now? Use a function.

  2. Need to process a collection at your own pace? Use an iterator.

  3. Need a single value from an async operation? Use a promise.

  4. Need to handle multiple values over time? Use an observable.

  5. 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!

0
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