The reactive programming is a paradigm oriented to data streams and the propagation of changes. Instead of continuously asking if something has happened, we describe how to react when a new value arrives.
The real world is not always sequential. Many systems receive information asynchronously. The user clicks when they want, the server responds when it can, and the GPS sends us coordinates whenever it feels like it.
Trying to model this with while loops or the hell of callbacks is painful. That’s where Reactive Programming fits in.
Instead of requesting data (Pull), we sit and wait for the data to come to us (Push), and we react to it.
The Excel Metaphor
The best way to understand reactivity is to think of a spreadsheet.
- Imperative approach:
You have cell A1 with
10and B1 with20. In C# you would do:var c1 = a1 + b1;.c1is 30. If you now changea1 = 50,c1is still 30. You have to re-run the sum line to update it. - Reactive approach (Excel):
In cell C1 you write
=SUM(A1, B1). The moment you change A1, C1 updates automatically.
In reactive programming, variables are not static boxes; they are streams of values over time. C1 is “subscribed” to changes in A1 and B1.
What is Reactive Programming
Formal definition:
Reactive programming models values that change and propagates those changes to those who depend on them.
With libraries like Rx, we can treat a click, a timer, or an HTTP response as an event stream. Not every reactive stream has to be asynchronous, although that is one of its most frequent uses.
And the best part: we can apply functional operators to them. Yes, the same ones we saw in the previous article (Map, Filter, Scan).
The Components of Rx (Reactive Extensions)
The most famous implementation of this paradigm is the Reactive Extensions (Rx) family, available in almost all languages (RxJS, Rx.NET, RxJava/Kotlin).
Rx combines ideas from two well-known interfaces:
- Observer: the producer sends values to the consumer.
- Iterator: serves as a contrast; instead of requesting the next element, the observable delivers it when it is available.
In Rx, we have three main actors:
It is the data source. It is the “faucet” that emits events over time. It can emit three things:
- A Value (Next).
- An Error (Error).
- A Completed signal.
It is the listener. It defines what to do when data arrives, when there is an error, or when the stream ends.
They are pure functions that transform the stream before it reaches the subscriber. That’s where the interesting part lies.
Practical Example: The Predictive Search Bar
Let’s look at the quintessential use case: The Search Bar. We want, when the user types, to search on the server. But with conditions:
- Do not search if the text has less than three letters.
- Wait for the user to stop typing to avoid making a request for every keystroke.
- Do not repeat two consecutive searches with the same text.
- Ignore the previous result if a new search begins.
The Imperative Version with Callbacks and Timers
(This is pseudocode to illustrate manual management; it is not intended to compile.)
// ❌ "SPAGHETTI" IMPERATIVE CODE
private Timer _timer;
private string _lastSearch;
void OnTextChanged(string text) {
if (text.Length < 3) return;
// Manual timer management for debounce
if (_timer != null) _timer.Stop();
_timer = new Timer(500);
_timer.Elapsed += (s, e) => {
if (text == _lastSearch) return;
_lastSearch = text;
CallApi(text, (result) => {
// Be careful with race conditions here...
// What if another response arrives before this one? Chaos!
Show(result);
});
};
_timer.Start();
}
The Reactive Version with Rx.NET
Using System.Reactive and LINQ.
// ✅ REACTIVE CODE
// Convert the UI event into an Observable (a stream of strings)
var searches = Observable.FromEventPattern<TextChangedEventArgs>(txtSearch, "TextChanged")
.Select(evt => ((TextBox)evt.Sender).Text) // Extract the text
.Throttle(TimeSpan.FromMilliseconds(500)) // Wait for 500ms of silence (Debounce)
.Where(text => text.Length >= 3) // Filter short texts
.DistinctUntilChanged() // Ignore if it's the same as the previous one
.Select(text => CallApi(text)) // Project to the API call
.Switch(); // If a new one arrives, cancel the previous one
// Subscribe to the final result
searches.Subscribe(result => {
Console.WriteLine($"Results received: {result.Count}");
});
Notice the difference. We have described the logical flow of the data. There are no temporary variables, no manual timer management, and no nested callbacks.
The .Switch() operator unsubscribes from the previous observable when a new one arrives. The underlying operation is only truly canceled if the observable implements that cancellation; in any case, its result stops being delivered to the subscriber.
Hot and Cold Observables
This is a technical concept that often confuses people at first.
-
Cold observables: They are like playing a movie on demand. The sequence begins when you subscribe, and each subscriber gets its own execution.
-
Example: an HTTP call or reading a file.
-
Hot observables: They are like the radio or a live sports game. The emission occurs regardless of your subscription, and if you arrive late, you miss the beginning.
-
Example: mouse clicks or a real-time stock quote.
When to use Reactive Programming?
Do not use Rx to add two numbers. Use it when you have asynchronous complexity:
- Complex UI events: drag and drop, key combinations, or real-time validations.
- WebSockets and SignalR: data arriving in real-time from the server.
- Composition of asynchronous operations: combining, transforming, or recovering streams with operators like
Merge,Zip, andCatch.
Reactive programming requires a change of mindset. You stop thinking in terms of “steps” and start thinking in terms of “pipelines” through which data flows.
It is difficult at first. You will look at a Marble Diagram and not understand anything. But once you master operators like FlatMap (or SelectMany in C#), you will realize that traditional imperative code seems prehistoric for managing events.