Reactive Programming, Angular Signals, and Johnny's Candies

Seweryn Wawrzynowicz

Seweryn Wawrzynowicz

· 12 min read
Thumbnail

Johnny's Candies and Reactive Programming

For many people, reactive programming might sound mysterious or even intimidating. However, behind this seemingly complex term lies something remarkably simple and intuitive. To demonstrate this, I’ll use a straightforward analogy from everyday life involving handing out candies in a classroom. Imagine Johnny bringing candies to school for his birthday. In the classic (imperative) approach, Johnny has to walk around the classroom, asking each student whether they’ve already eaten a candy and if they need another one. This process is time-consuming and inefficient. In contrast, in the reactive approach, Johnny simply stands in the middle of the classroom and waits. Students who want another candy raise their hands. This way, Johnny only reacts when someone actually requests a candy, saving both his time and energy.

Below, I’ll demonstrate these two approaches in TypeScript code: the classic approach and the reactive approach using Angular Signals.

The Classic Approach

In the classic approach, we need to continuously check if students need candies, which can be illustrated with a loop.

1type Student = { 2 name: string; 3 needsCandy: boolean; 4}; 5 6const students: Student[] = [ 7 { name: 'Ania', needsCandy: true }, 8 { name: 'Tomek', needsCandy: false }, 9 { name: 'Zosia', needsCandy: true }, 10]; 11 12function giveCandyToStudents() { 13 // Johnny continuously checks if someone needs a candy 14 setInterval(() => { 15 students.forEach((student) => { 16 if (student.needsCandy) { 17 console.log(`${student.name} gets a candy!`); 18 student.needsCandy = false; // Student receives a candy 19 } 20 }); 21 }, 1000); // Checking every second 22} 23 24giveCandyToStudents();

In this example, Johnny has to go through all the students every second to check if anyone needs a candy. Even if no one needs candies, the process repeats, which is inefficient.

The Reactive Approach with Angular Signals

In the reactive approach, students signal their need for a candy, and Johnny reacts only to those signals. This can be implemented using Signals in Angular.

1import { signal, effect } from '@angular/core'; 2 3const students = signal([ 4 { name: 'Ania', needsCandy: false }, 5 { name: 'Tomek', needsCandy: false }, 6 { name: 'Zosia', needsCandy: false }, 7]); 8 9// Function that gives a candy to a specific student 10function giveCandy(index: number) { 11 const updatedStudents = students().map((student, i) => { 12 if (i === index) { 13 console.log(`${student.name} gets a candy!`); 14 return { ...student, needsCandy: false }; // Student receives a candy 15 } 16 return student; 17 }); 18 students.set(updatedStudents); 19} 20 21// Reactive approach: only react when the state changes 22effect(() => { 23 students().forEach((student, index) => { 24 if (student.needsCandy) { 25 giveCandy(index); // Give a candy only to those who raised their hand 26 } 27 }); 28}); 29 30// Signaling the need for a candy (a student raises their hand) 31setTimeout(() => { 32 const updatedStudents = students().map((student, index) => { 33 if (index === 0) return { ...student, needsCandy: true }; // Ania raises her hand 34 return student; 35 }); 36 students.set(updatedStudents); 37}, 2000); 38 39setTimeout(() => { 40 const updatedStudents = students().map((student, index) => { 41 if (index === 2) return { ...student, needsCandy: true }; // Zosia raises her hand 42 return student; 43 }); 44 students.set(updatedStudents); 45}, 4000); 46

In this example, Johnny doesn’t need to continuously check on the students—the system "knows" when someone needs a candy (when a student's state changes to needsCandy: true). Thanks to effect, Johnny automatically reacts to the changes and gives a candy to exactly the person who requested it.

Why Are Signals Better Than RxJS?

RxJS is a powerful tool, but its complexity can be overwhelming, especially for people who are just starting with Angular. Below, I’ll explain the advantages of Signals over RxJS, using the candy example to make it more relatable.

1import { BehaviorSubject } from 'rxjs'; 2 3const candyBasket = new BehaviorSubject(['Candy 1', 'Candy 2', 'Candy 3']); 4 5candyBasket.subscribe(basket => { 6 console.log(`Candies left: ${basket.length}`); 7}); 8 9candyBasket.next(['Candy 1', 'Candy 2']); // Console: "Candies left: 2"
  • You need to remember to manage subscriptions (e.g., using unsubscribe at the appropriate time).
  • You have to manually update the state (candyBasket.next(...)).

Signals: With Signals, you don’t need to manage subscriptions or manually refresh data.

Better understanding of Angular Signals

A Signal is used to store and manage the state. In this case, the state is the list of candies in the basket. Here’s how we create a Signal:

1import { signal } from '@angular/core'; 2 3// Initial state: a basket with 3 candies 4const candyBasket = signal(['Candy 1', 'Candy 2', 'Candy 3']); 5 6// Access the value of the Signal 7console.log(candyBasket()); // Output: ['Candy 1', 'Candy 2', 'Candy 3'] 8
  • **signal() ** creates a reactive state.
  • Calling candyBasket() <u>reads the current value</u> of the Signal.

Step 2: Updating the Signal

Signals provide methods to update the state:

  • set(newValue): Replaces the current value with a new one.
  • update(mutatorFunction): Modifies the current value in place.

Let’s simulate someone taking a candy from the basket:

1// Someone takes the first candy (removes the first item from the array) 2candyBasket.update(basket => basket.slice(1)); 3 4// Access the updated value 5console.log(candyBasket()); // Output: ['Candy 2', 'Candy 3']
  • basket.slice(1) creates a new array without the first element.
  • The update() method applies the change to the Signal’s state.

Step 3: Automatically Reacting to Changes Using Effect

Now let’s make the system "reactive". We’ll use an Effect to automatically respond to any change in the state of the candy basket.

1import { effect } from '@angular/core'; 2 3// Automatically log the number of candies whenever the basket changes 4effect(() => { 5 console.log(`Candies left: ${candyBasket().length}`); 6}); 7 8// Simulate changes 9candyBasket.update(basket => basket.slice(1)); // Console: "Candies left: 2" 10candyBasket.update(basket => basket.slice(1)); // Console: "Candies left: 1"

-The effect() function observes the Signal (candyBasket) and automatically triggers whenever its value changes.

  • You don’t need to manually "subscribe" or "unsubscribe" as you would with RxJS.

Step 4: Adding Computed Values

A Computed value lets you define derived data that automatically updates whenever the underlying Signal changes. For example, let’s track whether the candy basket is empty:

1import { computed } from '@angular/core'; 2 3// A computed value to check if the basket is empty 4const isBasketEmpty = computed(() => candyBasket().length === 0); 5 6// Check the computed value 7console.log(isBasketEmpty()); // Output: false 8 9// Simulate all candies being taken 10candyBasket.set([]); // Empty the basket 11console.log(isBasketEmpty()); // Output: true 12
  • The isBasketEmpty value is automatically recalculated whenever the candyBasket Signal changes.
  • No manual updates or extra code are needed.

Why Signals Are Better

In this example:

  1. Automatic Updates: Effect and Computed automatically respond to changes in the Signal. You don’t need to manually track changes or call additional functions.
  2. No Subscriptions: There’s no need to subscribe or unsubscribe from state changes, unlike RxJS.
  3. Simple API: The signal, computed, and effect functions are straightforward and easy to use. This simplicity and automation make Signals a great alternative to RxJS for managing state in Angular. 😊
Copyright © 2025 . All rights reserved.
Blog created by SevDev using the template from Web3Templates