RxJS sits in that weird space where it's incredibly powerful but also where most developers initially go "wait, what?" The learning curve is real. But once it clicks, you realize you've been solving asynchronous problems the hard way your entire career.
What reactive programming actually means
Reactive programming is about thinking in streams. Instead of "get data, then do something, then do something else," you declare "when this value arrives, transform it, combine it with that value, and push it downstream." The framework handles the orchestration. You describe relationships between data, not step-by-step instructions.
It's declarative instead of imperative. That sounds abstract, but it's the difference between "give me all the users, then filter for admins, then format them" versus "map users to admins through filters and format." The second one is easier to reason about because you're composing small, testable pieces instead of tracking state through an imperative sequence.
The core pieces
Observables are the foundation. Think of an observable as a channel that emits values over time. It could emit zero values, one value, or thousands. It could emit immediately or wait forever. It could complete cleanly or error. User clicks are observables. HTTP responses are observables. Timer events are observables. Anything that happens over time can be an observable.
Observers are the things listening to observables. An observer has callbacks: one for when a value arrives, one for errors, one for when the stream completes. You subscribe to an observable by giving it an observer.
Operators are where RxJS gets powerful. They transform observables. Map applies a function to each value. Filter keeps only values that pass a test. MergeMap flattens nested observables. CombineLatest waits for the latest value from multiple streams. There are dozens, and you compose them together. This is where complexity becomes elegant if you think in streams.
Subjects blur the line between observer and observable. A subject is both. You can push values into it, and it broadcasts them to everyone subscribed. Useful when you need to turn imperative code (like "user clicked here") into reactive streams.
Schedulers control when things happen. Do notifications fire immediately or get queued? Do they happen on the main thread or in a worker? For most cases you don't think about schedulers, but they're there if you need fine-grained control.
Where it actually helps
Search input is the classic example. As the user types, you want to fetch matching results. But you don't want to fetch on every keystroke. You want to wait until they stop typing for a moment, and you don't want requests trampling each other if they're slow. With RxJS, you compose operators: debounce to wait for pauses, distinctUntilChanged to skip duplicate values, switchMap to cancel previous requests when new input arrives. A few lines of clean, declarative code.
Real-time data is another killer use case. Stock prices, chat messages, sensor readings. These are inherently streams. You might need to combine multiple streams, react when values cross thresholds, buffer events into batches. RxJS makes this natural.
Form validation across multiple fields, where one field depends on another, is messy in traditional approaches. With RxJS, you model each field as an observable, combine them, and describe the validation logic declaratively.
The real cost
Learning RxJS requires rewiring how you think about asynchronous code. That's not a small thing. Error handling is different. You need to understand marble diagrams to visualize what operators do. Debugging async streams is harder than debugging sequential code. It's powerful, but it's not free.
You also don't need it everywhere. Simple CRUD operations with fetch don't benefit. RxJS shines when you have complex asynchronous workflows, high-frequency events, or streams that need transformation and coordination. Use it where it actually solves a problem, not as a generic wrapper for all your code.
But for the problems it solves, it's genuinely the best tool. Once you've handled complex async with RxJS, going back to callbacks or promises for the same type of problem feels clunky. That's when you know the paradigm shift actually paid off.