DEV Community

EsdrasSantosDV
EsdrasSantosDV

Posted on

The Article I Took So Long to Write: Concepts about Reactive Programming and RxJS

Since I graduated from college this year, I've always wanted to write some articles about technology, but I was unsure about what the first topic should be. Then I thought: what was one of the technical challenges that gave me the most trouble when I started professional development during my internship, and that could help other developers who are just starting out? Right away, one of the things that gave me the most difficulty at the beginning was understanding reactive programming in the universe of the Angular framework. Much of this difficulty came from the reactive library RxJS. Although RxJS isn't exclusive to Angular, it's essential for those who develop with the framework, and understanding reactive programming was a game-changer. I remember that one of the first tasks I did involved working with Observables in some search filters, and without fully understanding the concept of reactivity, that task seemed like a nightmare. Gradually, I got the hang of it, and now, with this article and the ones to come, I hope to share this knowledge with other developers.

So, what exactly does it mean to "be reactive" in programming? Basically, reactive programming is a declarative paradigm, like functional, modular, procedural, or object-oriented programming. But what is a "programming paradigm"? In summary, it's a set of rules and principles that guide how you write your code. It's kind of the "style" of programming, something that dictates the approach and methodology. Meanwhile, architectural and design patterns are like reusable scripts or templates for organizing your code and solving common problems.

Now, about reactive programming: it deals with data flows and the propagation of changes. It might sound complicated, but the central idea is simple—you are essentially reacting to changes that can happen at any time, such as events or data modifications. The big idea is that, with reactive programming, the parts of the code that depend on these changes are automatically notified and updated. You don't have to manually check if something has changed, which ends up greatly simplifying the data flow in more complex applications.

Talking with other developers, I realized that this was also a challenge for many, and even today, I see some programmers not fully understanding the concepts. At times like this, I always remember a great professor I had during my undergraduate studies, who used to say: "If you really want to solidify the concept and see if you truly understand it, try explaining it to someone outside the field, like your girlfriend or a relative." Since my girlfriend is also a programmer, I often don't need to contextualize certain concepts, but now I'm going to try to explain reactive programming in a way that makes sense to my father, who isn't in the programming field. I always try to test with him or my mother to see if I can convey the concept simply.

Subscribe channel

"Father, since you really enjoy Cariani's channel on YouTube, instead of going on YouTube every day to see if he has posted a new video, you can subscribe to the channel. When you do that, YouTube knows you want to be notified whenever there's a new video.

So, every time Cariani posts something, YouTube sends you an automatic notification on your phone or email, letting you know that the video is now available. This saves you time because you don't need to keep checking the channel every day—you only get notified when something genuinely new happens. And most importantly, YouTube won't send this notification to everyone, only to people who have subscribed to the channel. So those who are interested in the content, like you, will be notified, but those who haven't subscribed won't receive anything.

This "subscription" and "notification" scheme on YouTube is very similar to reactive programming. In code, we "subscribe" to be notified of events or changes—like when the status of an order changes or a server response arrives. From that point on, the code is only "notified" when something new happens, just as YouTube notifies you when there's a new video. This makes the code much more efficient, working only when it needs to."

Unsubscribe

And, just like on YouTube, you can unfollow the channel and stop receiving notifications. In reactive code, you can also "unsubscribe" when you no longer need to monitor changes.

Now, if we think about reactive systems, the concept is similar. They optimize resources by "subscribing" and processing data only when something actually changes, like in the YouTube notifications example. This avoids the unnecessary work of constantly checking, which helps improve the system's overall performance. The system only acts when something relevant happens, making it more efficient and agile.

This approach is ideal for making a system more responsive and scalable because it uses resources only at the right moment, saving processing power and memory. Instead of overloading the system with continuous checks, it conserves computational resources. This is especially useful when dealing with asynchronous data flows that happen unpredictably, such as a server response or user interaction.

Some practical examples where reactive programming stands out are in HTTP requests (like when a website requests data from a server), form changes (when you fill out a field and the system reacts automatically), and browser events like clicks, mouse movements, and key presses. These events are unpredictable, and the system needs to react immediately when they occur without the need to constantly check.

Reactive programming has a very interesting aspect, which is the ability to compose. This allows developers to build more complex behaviors from simple and well-defined operations. By using various tools that allow combining, transforming, and organizing data flows—like operators to merge multiple data streams simultaneously—you can handle these flows efficiently.

This modular approach is extremely useful when creating more flexible and well-organized applications. Distinct data flows can be integrated and adjusted, resulting in more sophisticated systems that are easy to maintain and adaptable. Additionally, the ability to join small pieces of logic contributes to code reuse, encouraging the development of highly scalable applications that can evolve over time without major rewrites.


 searchTerm$ = this.searchField.valueChanges.pipe(
  startWith(''),
  debounceTime(300),
  distinctUntilChanged(),  // Ignores searches that haven't changed
  filter(term => term.length >= 2)  // Only searches terms with more than 1 character
);

// Category selection stream (Hero or Villain)
categoryTerm$ = this.categoryField.valueChanges.pipe(
  startWith('')
);


// Combining multiple streams
filteredCharacters$ = combineLatest(this.searchTerm$, this.categoryTerm$)
.pipe(
  switchMap(([searchTerm, categoryTerm]) =>
    this.characterService.searchCharacters(searchTerm).pipe(
      // Filtering the final result based on category
      map(characters =>
        characters.filter(character =>
          categoryTerm === '' || character.category.toLowerCase() === categoryTerm.toLowerCase()
        )
      )
    )
  )
);

Enter fullscreen mode Exit fullscreen mode

This code creates a reactive search system for Dragon Ball characters, combining two data streams: one for the character's name and another for the category, such as Hero or Villain. The search field only performs the search after 300ms of inactivity, avoiding unnecessary searches and ensuring that very short or repeated terms don't trigger new requests. The category field allows filtering results according to the user's choice between Heroes and Villains. These streams are combined, and the search is automatically updated when the name or category is changed, filtering first by name and then by category. The system is dynamic, responding in real-time to changes and updating the list of characters displayed according to the user's preferences.

Another important aspect of reactive programming is managing events and data changes in a non-blocking manner, which is precisely where the performance gains stand out. With non-blocking code, multiple tasks, events, and changes can be executed in parallel. In other words, the code doesn't need to wait for a task to finish to continue; it proceeds to the next instructions immediately while asynchronous tasks are processed in the background. In contrast, with blocking code, the system only moves to the next line after completing the previous task, which can cause delays and reduce the system's efficiency.

In my experience, this became even clearer when I returned to studying Java on the backend. I realized that reactive programming wasn't limited to Angular or the frontend. On the backend, reactive programming plays a crucial role, especially in earlier versions of Java where we didn't yet have support for virtual threads. In these scenarios, efficiently managing threads is fundamental in high-load environments, such as web systems that need to handle thousands of simultaneous requests. I will delve deeper into the impact of virtual threads and how they change the game in thread management in a future article, but the important thing here is that, before their introduction, reactive programming was already an essential solution to ensure scalability and performance in complex systems.

In Java, the use of threads has always been a powerful feature but also a challenge because creating and managing many threads can consume a lot of memory and overload the system. This is where reactive programming comes in, using non-blocking code so that the system doesn't need to create a new thread for each request or asynchronous task. Instead, the system efficiently reuses threads, allowing it to handle more simultaneous requests without needing to allocate additional resources.

Working with frameworks like WebFlux and Quarkus/Mutiny, I saw how this works in practice. These frameworks adopt reactive programming on the backend to build non-blocking APIs, where a single thread can manage multiple requests simultaneously. This allows for creating systems that respond faster, support more users at the same time, and utilize resources optimally. The experience I already had with RxJS on the frontend was essential to understand how to apply these concepts on the backend with Java, showing that reactive programming is not just for improving the user experience on the frontend but is also a powerful solution to enhance performance on the backend.

**

Now that I've lost myself explaining a little about where you can use reactive programming, let's go back to the application in Angular!

**

In Angular, reactive programming revolves around RxJS, which is the foundation of everything. RxJS, which stands for Reactive Extensions Library for JavaScript, is a library designed to handle reactivity in JavaScript. It comes integrated into Angular and is used by default.

With RxJS, you can create, consume, modify, and combine asynchronous data streams and events in an organized manner. At the core of RxJS, we have four main concepts: Observables, Observers, Subjects, and operators. I will cover Subjects in a separate article.

Note: All the concepts discussed shortly are used in other reactive libraries.

Reactive programming is an integral part of the Angular framework. Angular heavily relies on Observables (I will explain Observables in detail shortly) and has the RxJS library built-in to manage data streams robustly and composably. Observables are a reactive design pattern because you have the Observable, or data publisher, and the subscribers, who are the receivers of the data stream. Subscribers are automatically notified when the Observable emits a new value and can act.

Reactive programming is an integral part of the Angular framework. Angular heavily depends on Observables (I will explain Observables in detail in a moment) and has the RxJS library built-in to manage data streams in a robust and composable way. Observables are a reactive design pattern because you have the Observable, or data publisher, and the subscribers, who are the receivers of the data stream. Subscribers are automatically notified when the Observable emits a new value and can act accordingly.

Observables are used in many different aspects within the Angular framework. Here are some examples of where you can find Observables in Angular:

  • HTTP Requests: Within Angular, HTTP requests return Observables by default, whereas in pure JavaScript they are handled with Promises. This is something that remains the same for other libraries like React.
  • Router Events: The Router class in Angular exposes an Observable of events. With it, you can listen to routing events like NavigationStart, NavigationEnd, GuardCheckStart, and GuardCheckEnd.
  • Changes in Reactive Forms: In reactive forms and form controls, Angular exposes an Observable called valueChanges. With it, you can automatically react to changes within the form or in specific fields. Now in version 18, Angular also has an Observable for state changes of the form control, indicating if it is touched or valid.
myControl.events
  .pipe(filter((event) => event instanceof PristineChangeEvent))
  .subscribe((event) => console.log('Pristine:', event.pristine));

myControl.events
  .pipe(filter((event) => event instanceof ValueChangeEvent))
  .subscribe((event) => console.log('Value:', event.value));

myControl.events
  .pipe(filter((event) => event instanceof StatusChangeEvent))
  .subscribe((event) => console.log('Status:', event.status));

myControl.events
  .pipe(filter((event) => event instanceof TouchedChangeEvent))
  .subscribe((event) => console.log('Touched:', event.touched));


Enter fullscreen mode Exit fullscreen mode

Most state management libraries in Angular, such as NgRx, NGXS, Akita, and Angular Query, also use RxJS as their foundation. These libraries rely on Observables and reactive programming to facilitate efficient and predictable state management, fully leveraging the composition of data streams and the reactivity provided by RxJS.

What are Observables?

Observables are the core of RxJS. You can think of Observables as a stream or pipeline that emits different values asynchronously over time. To receive the values emitted by the Observable's data stream, you subscribe to the Observable.

Now, imagine an Observable data stream as a water pipe. When you turn on the tap (subscribe), the water (data) flows through the pipe (Observable), and you receive drops of water (values) at your end. The water (data) might already be flowing before you turn on the tap (subscribe), unless you have installed a special system; you will only receive water (data) from the moment the tap is open (subscribe) until you close it (unsubscribe). The water (data) can continue flowing to other taps (subscribers) if they are open. In summary, to receive values from the data stream, you need to subscribe, and all values emitted before you subscribe will be lost unless you have special logic to store those values.

To stop receiving values, you need to unsubscribe, and the data emitting the values flows like a river.

What are Observers?

Observers are the entities that subscribe to an Observable and receive the values from that Observable in the data stream. You can think of the Observer as the person (or subscriber) watching a live program or streaming a movie on Netflix. Subscribers have two tasks: to subscribe to the streams they want to receive and to unsubscribe from the streams they no longer need or want to receive.

One of the most crucial aspects is properly unsubscribing from subscriptions that the Observer no longer needs. Not unsubscribing from Observables is probably the biggest risk when using Observables in your code and is the most common source of problems. You may end up facing memory leaks if you don't clean up your subscriptions correctly. Memory leaks will result in strange behavior, a slower application, and eventually may cause your app to crash if the memory runs out.

Let's imagine that you have a subscription to a comic book magazine delivered to your home every week. If you move to a new address, you'll need to cancel the subscription to the old address and create a new subscription for the new address. If you don't cancel the old subscription and simply start a new one for the new address, you'll start paying double, and the magazine will be delivered to both addresses. If you keep repeating this process, you'll end up without money, and your life will collapse. In the case of your applications, it's the same, but instead of paying with money, you "pay" with memory.

When you create a subscription inside a component, you need to unsubscribe when the component is destroyed. Otherwise, the subscription will continue running. The next time you open the same component, a second subscription will start because the old one is still active. As a result, all values will be received by two Observers. If you keep repeating this process, you'll end up with many Observers receiving the same values when, in fact, you only need a single Observer to receive the data. When there are too many Observers, the application will run out of memory to process all the values, and the app will eventually crash.

Once, I myself forgot to "kill" some subscriptions in a project, and this caused a really annoying problem. Every time I opened that screen, it started to freeze everything. What was happening was that by not unsubscribing from the Observables, several old subscriptions remained active even after I closed the screen. And the worst part was that this happened in a loop. Every time the screen was opened, more subscriptions were created, and the old ones kept running. Over time, the number of subscriptions became so large that it overloaded the application, slowing everything down and eventually crashing everything.

A fact that few people know is that, in Angular, an HTTP request subscription does not need to be manually unsubscribed because it "auto-unsubscribes" when the request is completed. This already helps to avoid some memory leak issues. Additionally, another practice that I always maintain is the use of the async pipe, which greatly facilitates automatically managing subscriptions and writing code in a more declarative way. In fact, this will be one of the topics of another article I am planning, where I will talk more about how to adopt a declarative approach in Angular using these practices

What is .pipe()?

Before putting the cart before the horse with the filter example, I applied certain transformations. Now, let's explain and contextualize what this really means and why this process is so important to understand. Pipeable operators function like an assembly line in a factory. Think of the Observable as raw material entering the production line. Each station in this factory is an operator that does something with the material: some clean impurities, others adjust the shape, and others transform the material into something completely new.

Industries

Just like in a factory where the product goes from station to station until it's ready, the data from the Observable passes through different operators within the pipe() function. Each operator does its part: it can filter out irrelevant information, transform the data, or even modify the value before it reaches the final result. At the end of the line, what comes out is a modified Observable, without altering the original Observable that was the initial "raw material."

 // Observable que emite uma sequência de números seria a materia prima
const numbers$ = of(1, 1, 2, 3, 4, 4, 5, 6, 7);

// Usamos a função pipe() para aplicar vários operadores
numbers$
  .pipe(
    // Remove valores consecutivos duplicados
    distinctUntilChanged(),
    // Filtra os valores menores ou iguais a 5
    filter(value => value <= 5),
    // Multiplica os valores restantes por 10
    map(value => value * 10),
    // Limita a 3 primeiros valores do fluxo
    take(3),
    // Soma os valores ao longo do tempo
    scan((acc, value) => acc + value, 0),
    // Aplica uma ação colateral (log) em cada valor emitido
    tap(value => console.log('Valor atual após scan:', value)),
    // Pega apenas o último valor emitido
    takeLast(1)
  )
  .subscribe(result => console.log('Resultado final:', result));

  }

Enter fullscreen mode Exit fullscreen mode

Factory code

Just like in an industry where each station has a specific function to ensure the product comes out perfect, pipeable operators allow you to manipulate the data stream efficiently, adjusting whatever is necessary along the way.

Types of Observables: Hot and Cold

Another very important concept is the types of Observables: Hot and Cold, which are crucial for understanding the transmission of data streams over time.

Cold Observables are unicast, meaning they start from zero for each subscriber. It's like a movie on Netflix; when someone starts the movie, it begins from the beginning. If someone else starts the same movie on another account or TV, the movie will also start from the beginning. Each person watching has their own unique experience from the moment they start watching.

On the other hand, Hot Observables are multicast, which means there is a single data stream broadcasted to all subscribers. Hot Observables can be compared to live television. Different people can tune into the live program (the data stream), but anything you've already missed is gone and won't be replayed for you. Everyone watching experiences the same content simultaneously, even if they didn't start watching from the beginning.

Final

This article was a general introduction to the initial concepts of reactive programming, especially in the context of Angular and RxJS. The focus was on providing an overview of what reactive programming is and how it can be a game-changer for those who are starting out, without delving too much into code structure or the implementation details of RxJS. The idea was to discuss the concepts of Observables and reactivity more broadly since many of these concepts apply to other reactive libraries, not just RxJS.

I covered more of the theoretical concepts and used analogies to facilitate the understanding of reactive programming without getting too deep into code or the detailed implementations of RxJS. The aim was to build a solid foundation of comprehension before diving into future articles with practical code examples and more complex implementations. This way, when we get to articles more focused on development, you'll already have a good theoretical base to follow along.

This is just the beginning of a series of articles where I will dive even deeper into the world of reactive programming, which is full of details and challenges. Later on, I want to talk about reactivity on the backend, explaining better how it works in Java. I will also cover topics such as blocking vs. non-blocking code, thread management, the difference between coding in a declarative and imperative manner, higher-order operators, Subjects, and so on.

Now that I've written my first article outside the scientific format, I realized that I can express myself more simply, without seeming like I'm writing a thesis. This motivated me to continue writing in a more relaxed and accessible way. The more I share knowledge, the more I will be learning too, and it's this exchange that makes it all worthwhile.

Furthermore, I also want to address topics like software architecture, design patterns, and other technologies that help in developing robust and scalable applications. I'll try to bring these articles in English as well, both to reach a larger audience and to improve my English, which is pretty rough.

So, if you enjoyed this and want to learn more, subscribe to my profile to keep up with the upcoming posts! 😉 Let's go together!

If you have any questions or want to chat more about programming in general, feel free to reach out to me on LinkedIn, and here's also my GitHub if you want to follow any project or study. I'm always open to exchange ideas and discuss development!

Hey, PH! For you who asked me for the article, here it is!

Top comments (0)