Could promises do better?
For the majority of Angular developers, RxJS was an unfamiliar concept until Angular made learning it a prerequisite for some fundamental APIs.
Initially, observables appeared to be a slightly enhanced version of promises—they could deliver multiple values over time, which promises couldn't. So the expectation was straightforward: fetch HTTP data with something like http.get(...).subscribe(data => this.data = data), handle websocket messages similarly, and that would cover the usefulness of RxJS. The Angular team probably held this view as well.
However, the capacity to model long-lived data streams proved to be a game-changer beyond what promises offered. Observables open the door to functional reactive programming (FRP), where asynchronous logic is expressed declaratively. This shift is significant. FRP can completely wipe out race conditions and inconsistent state, boost code structure, speed up initial loads, simplify state handling, and clarify naming choices. My previous article dives into all of this in detail.
The Angular community fractures
Adopting FRP demands a shift in perspective to reap its rewards—a shift often called "thinking reactively." For many Angular devs, that moment came with their first encounter of NgRx/Store.
Rob Wormald built NgRx in 2016 as the go-to state management solution for Angular, merging Redux with RxJS. This union made sense: Redux had traction in the React world, and its store already exposed a subscribe method, much like observables do. It was essentially a natural fit for RxJS implementation.
NgRx looks really cool and powerful. It's probably super easy to use. Oh wait, how do I grab the current state? Can't I just do
this.store.currentState?
No, you can't.
And there was no plan to change that, nor any intention to consider such a plan.
That became clear as an RxJS philosophy emerged. Digging deeper, we ran into surprising guidance like "don't unsubscribe" and "don't subscribe". Imagine if someone advised you to skip .then entirely with promises. But with RxJS, the guidance is to reach for withLatestFrom, takeUntil, and Angular's async pipe instead. The claim is that relying on the async pipe can make our apps faster.
This created a critical juncture for Angular developers: Is there something I need to grasp here, or is RxJS just impractical because my first instinct doesn't work?
Leaving NgRx behind vs. growing beyond it
Shortly after NgRx injected more RxJS into Angular apps, alternatives emerged for those who preferred not to adopt the reactive mindset, including NGXS and Akita.
The divide grew as some developers embraced RxJS fully. I recall a podcast where Rob Wormald (NgRx's creator and an Angular team member) painted a vision of a zoneless Angular with RxJS streams flowing straight from event sources into templates.
That idea captured my imagination. I pictured a future where RxJS streams drove precise DOM updates directly, making change detection unnecessary. That would give Angular a massive performance edge, already matching React's speed. I hoped such a boost would lure more developers into reactive thinking, unlocking additional benefits like eliminating race conditions, cleaner code organization, faster loads, easier state management, and more meaningful function names.
Yet that future seemed distant, as I continuously wrestled with both NgRx and Angular while writing reactive logic.
For instance, to aggregate data from 3 services in sequence for a page, RxJS handles it like this:
data1$ = this.http.get(...);
data2$ = this.data1$.pipe(switchMap(data1 => this.http.get(...));
data3$ = this.data2$.pipe(switchMap(data2 => this.http.get(...));
The strength here is that each data source is declarative, standing alone without caring how it gets used to shape other state or features. This offers incredible flexibility with all the FRP advantages I've already highlighted twice.
But when you try to feed that data into NgRx/Store, the standard approach steers away from reactivity, so you miss out on FRP's full payoff. Eventually, I devised a method to wrap the NgRx API (dispatch & select) within RxJS, letting me structure code exactly like pure RxJS. Here's how it fared against the typical NgRx/Effects pattern:
I applied this RxJS-first strategy to a feature that leaned heavily on effects, slashing code by 25+% and cutting page load plus render time by 90+%.
Adopting a pro-reactivity stance took longer to outgrow NgRx than rejecting it outright. But now we have RxJS-centric libraries like RxAngular and StateAdapt, both designed for maximum reactivity in Angular state management.
So, while NgRx and Angular both introduced devs to RxJS, their designs made full reactivity cumbersome. I've pointed out one NgRx issue, but Angular had plenty more.
Angular falls short on reactivity
It's clear RxJS was an afterthought in Angular's architecture. It handled event streams like component outputs, and even route parameters, which are evolving states. But component inputs—also changing states—were left out of the observable model. So RxJS in Angular feels like a patchwork of inconsistencies.
When I hear fellow Angular developers complain about the friction of using RxJS, I can genuinely relate. But the blame belongs with Angular, not RxJS. Tasks that take 4 lines of RxJS + Angular code can be done with just a single character in RxJS + Svelte. Angular is the only major framework where most component libraries force dialogs to open imperatively, blocking the async pipe and forcing manual subscription handling, which is less painful elsewhere.
I could expand on this further, and I have before, but the crux is this: Even with all the wrapper components and utilities I've crafted, I can't repair Angular's core APIs.
You've turned them against me!
For a long time, I had been eagerly anticipating Angular's deeper embrace of RxJS. That issue, among the most highly-voted in Angular's history, dated back to December 2015! The initial glimmer of progress emerged only when the Ivy compiler wrapped up in 2019, but after two more years of quiet, the issue was unceremoniously closed! The team's rationale? "Turning inputs into Observables would couple Angular more tightly with RxJS, and we're not keen on deepening that coupling."
Really?
Before closing, Minko Gechev left a final note, reiterating the community's division: "The community remains split on the question of using RxJS more or less with Angular." He wrapped up by saying, "We'll detail our strategy for more ergonomic RxJS APIs once we prioritize that initiative." For someone who'd been waiting half a decade for improved Angular tooling, this was exasperating, especially with no timeline attached. Moreover, the core justification was that numerous Angular developers harbor a distaste for RxJS, and I'm convinced a major driver behind that sentiment is Angular's clunky RxJS integration.
Then came word that an Angular team member was probing an entirely new reactive primitive. If the Angular crowd shies away from reactivity, why would swapping in a different reactive approach win them over? If the complaint is about how Angular integrates RxJS, how would a fresh primitive address that? It all seemed baffling to me.
Ryan Carniato
Adding to my unease was Ryan Carniato's persistent argument that RxJS isn't truly fine-grained reactivity. I knew RxJS was capable of fine-grained DOM updates, and Ryan knew that too—he had, after all, built an early SolidJS version atop RxJS. I'd been bugging him in his YouTube stream comments for weeks, and finally he offered an explanation that made something click for me.
His rationale was that he labels RxJS "coarse-grained" not because you can't achieve fine-grained updates, but because developers typically default to combineLatest with streams. Is it fair to brand RxJS as coarse-grained just because of common usage patterns? I'd argue no, but Ryan's underlying point was nonetheless compelling—it triggered a new connection in my thinking I hadn't made before.
Ah, right—RxJS is genuinely weak there
When I initially dove into NgRx, I aimed to use RxJS everywhere, leaning heavily on the map operator for derived state. But these days, derived state in nearly all NgRx setups relies entirely on selectors. So while modern NgRx merges state from multiple reducers like this:
const selectItems = createSelector(state => state.items);
const selectFilters = createSelector(state => state.filters);
const selectFilteredItems = createSelector(
selectItems,
selectFilters,
(items, filters) => items.filter(filters),
);
my original attempt looked like this:
items$ = this.store.select(state => state.items);
filters$ = this.store.select(state => state.filters);
filteredItems$ = combineLatest(
this.items$,
this.filters$,
([items, filters]) => items.filter(filters),
);
I felt pretty good about this RxJS plan, until my team lead dropped a console log into the filter function and watched it fire 28 times, prompting a "why?" I muttered back something like, "No clue, honest—FPR is supposed to boost performance."
My first fix was distinctUntilChanged, which trimmed some of the re-executions.
Next, I noticed that any observable chaining off filteredItems$ would force the filter to recalculate. The remedy? shareReplay(), or realistically publishReplay(), refCount() due to a quirky RxJS quirk. That too pared down some runs.
Finally, I spotted that when items and filters updated simultaneously, combineLatest would trigger twice—once per source. This was wasteful, and it also sparked state errors because managing the in-between phases where one input was fresh but the other was stale proved tricky.
I was desperate to get RxJS to cooperate here. As a junior dev then, I poured countless hours into brainstorming a workaround with RxJS, landing on schedulers as the likely answer, yet I couldn't figure out the execution.
At that moment, doubt crept in. Look at the effort required, and it remains clunky. Is building custom operators for this really worthwhile? Why doesn't RxJS streamline derived state handling? Wouldn't selectors be a simpler, equally viable route?
I wasn't alone—many Angular devs hit the same walls around that time, and we all arrived at selectors as the go-to for derived state in NgRx.
Selectors
Whenever a fresh state management library appears, I immediately look for selector support, or if it forces you into distinctUntilChanged, publishReplay, refCount, combineLatest, and debounceTime. Most either demand that, or you live with inefficiency. RxAngular bucks the trend by dropping the debounceTime requirement, offering coalesceWith—a welcome change.
I've never warmed to selector syntax, but they felt essential. So when I crafted my own state management library, StateAdapt, I hunted for a cleaner selector syntax. Here's my solution:
// NgRx:
const selectItems = createSelector(state => state.items);
const selectFilters = createSelector(state => state.filters);
const selectFilteredItems = createSelector(
selectItems,
selectFilters,
(items, filters) => items.filter(filters),
);
// StateAdapt:
const adapter = buildAdapter<State>()({})({
items: s => s.state.items,
filters: s => s.state.filters,
})({
filteredItems: s => s.items.filter(s.filters),
})();
It uses a proxy object s (serving double duty for state and selectors) that tracks which selector you access and generates a more optimized memoization than createSelector.
I've been quite pleased with this setup, believing Angular needed little beyond RxJS for events and async work, coupled with selectors for synchronous derived state.
Yet, as it turns out, selectors have their own flaws.
Colocating Selectors with State
Selectors can be organized into three distinct categories based on their role.
State-Driven Selectors
These selectors pull data directly from a state object without modification:
const selectItems = createSelector(state => state.items);
These should be exported alongside the reducers that manage the corresponding state slice.
Composition-Based Selectors
These selectors merge state from multiple reducers or stores, then compute new derived values. Because the resulting derived state doesn't naturally belong to any single top-level source, these selectors often carry significant business logic.
Template-Oriented Selectors
These take the output of the first two selector types and reshape the data to make it immediately usable within a template.
Suppose a state object represents a rectangle with a position like { x: 50, y: 200 }. To render that as a div with inline styles using style="left: 50px; top: 200px", a template-oriented selector is a sensible choice:
itemWithStyle: s => ({ left: `${s.x}px`, top: `${s.y}px` }),
<div [ngStyle]="store.itemWithStyle$ | async"></div>
This approach keeps the component logic lean. Furthermore, this selector is a pure function, so it can be tested in isolation by importing the state adapter and supplying the selector with input values. That's a significant advantage. Where else would a pure function fit better than in a selectors file, where it can be verified on its own rather than through a component? Since it lives inside the state adapter, it's automatically reusable; if the same logic were embedded directly in a template, you'd need to extract it manually for use in another location.
Having said that, after experimenting with placing template-oriented selectors inside state adapters for some time, I've concluded that coordinating the same responsibility across two separate locations introduces unnecessary friction for such a limited benefit. The derived selectors file ended up cluttered with mundane UI details, and those details were far more practical when positioned next to the template itself. Consider a scenario where the same rectangle needs to be drawn as an SVG element. Wouldn't it be simpler to adjust that logic right there in the template rather than jumping to a different file? Furthermore, when would this UI logic actually be reused? Likely only when you intend to reuse the component as well.
Yet, how do you compute efficient derived state within a component? With StateAdapt, it's feasible, though not trivially simple, and the process isn't particularly ergonomic when using NgRx-style syntax.
Component Inputs and the Diamond Problem
Handling component inputs presents another challenge for both RxJS and selectors.
For a while, I strongly desired component inputs to behave as observables. But a subtle issue emerges: if multiple inputs update simultaneously, each input observable emits sequentially. Consequently, if you need to combine these input values within a component, a combineLatest setup would trigger an emission for each individual input. Selectors don't offer a solution here, as component inputs lie outside the global store.
This issue is widely recognized as the diamond problem.
Interestingly, there is a reactive primitive that offers elegant syntax and resolves the diamond problem without difficulty — and it's not RxJS: Signals.
SolidJS Signal Syntax is Impressive
Below is a straightforward SolidJS component:
const CountingComponent = () => {
const [count, setCount] = createSignal(0);
const doubleCount = createMemo(() => count() * 2);
return (
<div onClick={() => setCount(count() + 1)}>
Double count value is {doubleCount()}
</div>
);
};
Here's the equivalent implementation using Angular and RxJS:
@Component({
selector: 'app-counter',
template: `
<div (click)="count$.next(count$.value + 1)">
Double count value is {{doubleCount$ | async}}
</div>
`,
})
export class CounterComponent {
count$ = new BehaviorSubject(0);
doubleCount$ = this.count$.pipe(
map(count => count * 2),
distinctUntilChanged(),
publishReplay(),
refCount(),
);
}
The SolidJS syntax has several clear advantages:
1. No Need to Decide on Derivation Early
In Angular, you must anticipate that you'll have derived state to justify using BehaviorSubject initially. With SolidJS, all mutable state is simply a signal, with no need to determine upfront if it will be used to compute further state. This eliminates the need to rewrite a signal's declaration later using different syntax.
2. Skipping Unnecessary Recalculations
SolidJS signals, by default, skip recomputation when their source state remains unchanged. This removes the need for distinctUntilChanged.
3. Avoiding Redundant Computations Across Consumers
With SolidJS, the number of derived signals doesn't affect performance; each derivation doesn't trigger separate recalculation for every consumer. RxJS, in contrast, executes a map function for each derived observable unless you explicitly apply publishReplay(), refCount().
4. Effortless Combination
Combining SolidJS signals doesn't require an eager dependency array like NgRx selectors or combineLatest do. You can simply specify the combination as c = createMemo(() => a() + b()).
Moreover, the RxJS issue where combineLatest fires once per input simply doesn't occur with signals. SolidJS signals ensure each dependency finishes executing prior to the derived signal running. The diamond problem is naturally and cleanly solved by SolidJS signals.
Let's put this into perspective with a side-by-side comparison using selectors. The SolidJS code from before:
const CountingComponent = () => {
const [count, setCount] = createSignal(0);
const doubleCount = createMemo(() => count() * 2);
return (
<div onClick={() => setCount(count() + 1)}>
Double count value is {doubleCount()}
</div>
);
};
Now, consider Angular with StateAdapt:
@Component({
selector: 'app-counter',
template: `
<div (click)="count.increment()">
Double count value is {{doubleCount$ | async}}
</div>
`,
})
export class CounterComponent {
count = adapt(['count', 0], {
increment: state => state + 1,
selectors: {
double: state => state * 2,
},
});
}
This is actually quite reasonable! But as you’ll see shortly, there's still more boilerplate than necessary.
An NgRx implementation would be too verbose to show fully, but you can picture defining the selector inside the component file:
@Component({
selector: 'app-counter',
template: `
<div (click)="increment()">
Double count value is {{doubleCount$ | async}}
</div>
`,
})
export class CounterComponent {
selectDoubleCount = createSelector(
selectCount,
count => count * 2,
);
doubleCount$ = this.store.select(this.selectDoubleCount);
constructor(private store: Store) {}
//...
}
Selectors are effective and performant, but I've yet to see them used with the same minimal syntax that signals offer.
Isn't it compelling to imagine Angular having a reactive primitive that's both performant by default and requires minimal boilerplate?
A proposal
Here's an idea:
@Component({
selector: 'app-counter',
template: `
<div (click)="count.set(count.get() + 1)">
Double count value is {{doubleCount.get()}}
</div>
`,
})
export class CounterComponent {
count = signal(0);
doubleCount = memo(() => this.count.get() * 2);
}
Since class properties are being assigned, SolidJS's tuple syntax isn't an option here, but this feels quite close.
There should also be a clean way to declare Angular inputs as signals:
@InputSignal: count!: Signal<number>;
RxJS interop remains essential
Signals excel at synchronizing derived state, yet RxJS is still required for asynchronous reactivity. Anyone with meaningful RxJS experience will see why immediately, and I go into much more detail in this article.
That said, it would be ideal if the Angular team made converting signals back into observables as seamless as possible:
export class CounterComponent {
count = signal(0);
doubleCount = memo(() => this.count.get() * 2);
delayedCount$ = this.count.pipe(delay(1000));
}
The pipe here would internally convert the signal into an observable and then forward the arguments to that observable's pipe method.
Because we're dealing with an observable, a lazy import could conceal that pipe() logic, ensuring the RxJS bundle isn't loaded until it's actually required.
Alternatively, Angular could adopt SolidJS's approach:
const CountingComponent = () => {
const [count, setCount] = createSignal(0);
// signal to observable (`from` imported from 'rxjs'):
const count$ = from(observable(count));
// observable to signal (`from` imported from 'solid-js'):
const countAgain = from(count$);
};
Summary and wrap-up
I hold strong opinions, but I also change them frequently. It's not about chasing every monthly JavaScript trend though. It has taken considerable experience and frustration to shape my views, and every new lesson builds upon the last.
Initially, I saw RxJS as only marginally better than promises.
After learning the benefits of reactive thinking, I wanted to apply RxJS everywhere.
Later, I realized that memoized selectors outperformed RxJS for state synchronization. For async logic though, RxJS was superb, and I wanted better APIs than what Angular and NgRx offered out of the box.
Then came Ryan Carniato's take on fine-grained synchronous reactivity, his discussions about colocation in Marko 6, and finally people's remarks on the old Angular issue about the diamond problem with component inputs. These ideas swirled in my head until a conversation about RxJS's limitations with Ryan. Everything then clicked: there was a real gap for a reactive primitive that wasn't RxJS or selectors.
So,
- RxJS covers asynchronous reactivity.
- Selectors are needed for reusing state management patterns with state adapters.
- A simple reactive primitive is needed for basic, local, reactive state synchronization.
As I continue developing StateAdapt, I'll need to figure out how this reactive primitive plugs into state management overall. I'll likely start by exploring its easiest integration with SolidJS.
Everyone has pieces of the puzzle. I'm confident the web development community will eventually craft elegant, declarative syntax and effective strategies for teaching developers the reactive mindset, making our industry more productive and enjoyable.
Thanks for reading!
I'd be interested in your personal journey with anything discussed in this post.
Cover photo by Casia Charlie: https://www.pexels.com/photo/sea-dawn-landscape-nature-2433467/



