Writing
Signals and the things that stopped breaking
October 16, 2026
There was a component at work that tracked one value four different ways. An
@Input() setter stashed it in a private field, ngOnChanges recomputed a
filtered list from it, a BehaviorSubject re-broadcast it for a child, and the
template read the result through an async pipe. Every piece made sense when
it was written. Together they produced a bug where the list and the count next
to it disagreed by one item, for one frame, only when the input changed twice
in quick succession. Nobody had done anything wrong. Four things were holding
the same truth, and they didn’t always update in the same order.
That’s the problem signals are built for, and the idea is small: a signal is a
value that knows who’s reading it. Read it inside a computed() or a template
and Angular remembers the dependency. Change it and only the things that
actually read it get recalculated. You stop telling the framework what to
update. It already knows.
The first win was almost embarrassing. That whole setter, ngOnChanges, and
subject routine turned into an input() and a computed(). The filtered list
became a function of the inputs instead of a copy of them, so there was nothing
to keep in sync, because nothing was being synced. The off-by-one bug didn’t
get fixed so much as it stopped being expressible. I enjoy those more than
normal fixes, and I don’t get many of them.
Then came the part nobody puts on the conference slide. effect() is right
there, it looks friendly, and it’s the easiest way to drag the old habits
into the new world. Watch one signal, write to another, and congratulations:
you’ve rebuilt ngOnChanges with nicer syntax and slightly worse debugging.
The rule I ended up with is that if an effect writes a signal, that signal
almost certainly wanted to be a computed(). Effects are for leaving the
signal world (logging, touching the DOM, talking to something outside), not
for moving data around inside it.
RxJS also didn’t go anywhere, and shouldn’t. Signals are good at “what is the
value right now.” They are not good at “what happened over time”: a search box
you want to debounce, an HTTP call you want to cancel when the next one starts,
a websocket that’s a stream of events rather than a value. That’s still
observable territory. What changed is where it ends. The stream does its
stream things in a service, toSignal() turns the result into a value at the
edge, and the component never has to know there was a pipe involved.
The migration itself was boring, which is the nicest thing I can say about a
migration. Leaf components first, since they have the fewest opinions about
everything around them. Everything was already OnPush, so the change
detection story barely moved. Services got converted when a component needed
them to, not as some grand rewrite weekend. At no point was the app half
broken. It was just half old for a while.
Looking back, signals didn’t remove much complexity. What they did was make the shape of it visible. Every piece of state is now either a source, something that gets set, or derived, something that gets computed, and the code says which out loud. Most of the bugs I used to chase lived in the gap where derived state was pretending to be a source. Signals closed that gap. The four copies of one value are now one value and a couple of functions, and the count and the list have agreed with each other ever since.
— Adi