ngRx the last piece in the Angular architecture puzzle
NG-NL 2017 · Amsterdam, Netherlands
by Asim Hussain · 1 February 2017
I gave this talk at NG-NL in Amsterdam to give people a working mental model of ngRx — Angular’s RxJS-based state-management library. If Angular is the architecture of your house, ngRx is the interior design. The hard part isn’t the functions; it’s the paradigm shift in how you think, so the bulk of the talk is theory, animation and unicorns, pulled together at the end by one tiny counter app.
AI-generated summary of my talk
Jump into the talk
- 0:00 Unicorns, Anvils and my daughter's edits
- 2:00 ngRx as the interior design of Angular
- 5:02 Why you should care: reason, test, debug
- 6:03 The lineage: Flux to Redux to ngRx
- 7:04 What is state? What is an application?
- 10:08 Move it all into one place: the store
- 12:11 Reading state with store.select and the async pipe
- 15:16 Changing state: actions and reducers
- 16:17 Pure functions, and why testing gets easy
- 21:24 The counter demo and the time-travel debugger
The unicorns are my daughter’s fault
I have to give a caveat up front: this deck has far more unicorns and Anvil effects than you’d normally see at a technical conference. I was practising the talk with my 11-year-old daughter and, after about a minute, she told me I needed jokes. When I said it was a room full of professionals, she asked if I could make a unicorn go pop on the screen. I thought no — then I thought, actually, yes I can. She discovered Keynote’s animations, including the ultimate one, Anvil (when you really want to make a point in Keynote, use Anvil). It was the first time she’d ever shown interest in anything I do, so I let her go wild. None of it relates to ngRx in the slightest.
Why you’d care: reason, test, debug
I open with the reasons, and I come back to them at the end. ngRx makes your application easier to reason about, easier to test, and easier to debug. By “reason about” I mean something specific: the time it takes to load the application into your head. When I program I get the whole thing loaded into my mind and then I fly around that little world — I’m in the zone. The faster you can load it, the more productive you are. And this matters because roughly 99% of developers spend their days working on code somebody else wrote. You walk in, you have to understand what on earth is going on, and we assume it takes about six months to onboard someone onto an application. Anything that shrinks that is worth a lot.
The lineage, and what “state” even is
ngRx didn’t appear from nowhere. In 2014 Facebook released Flux. In 2015 came its successor, Redux — mostly used in React, though you can use it in Angular via ng-redux. Then in 2016 we got ngRx, which isn’t just a wrapper around Redux; it’s a reimplementation of the core principles in a more Angular way. The clue is in the name: RX. ngRx is built on RxJS, Angular is built on RxJS, so it fits the framework — and the dependency-injection system — far more naturally.
All of these are patterns for handling state. I don’t assume knowledge, so: state is just all the variables sprinkled around your code — integers, strings, objects, arrays. The current value of all of them at any moment is the state of your application. An application is something that responds to events (time-based, user interaction, network), and in a traditional app an event calls a function, which reads a state variable, writes another, calls another function, fires another event, and on and on until some Boolean flips from false to true and a sidebar appears. That tangle is hard enough to follow in your own code, let alone someone else’s.
One object to hold it all
The big move in ngRx is that all of that scattered state collapses into one place — and not just the state, but the functions that manipulate it. It’s literally one object. In my ngRx projects I have a folder, unimaginatively called state, and inside it is all my state plus the logic that mutates it — which is really my business logic. I might still keep little UI variables dotted around (a Boolean that shows a sidebar isn’t core logic), but the heart of the app lives in one place. We call that the store. That’s what makes the app easier to reason about: everything is in one spot.
Reading state, and changing it
You don’t hand the whole store object to a component and let it pick whatever it wants. The component wants one tiny part, so you take a slice using store.select. The store is a service you inject; select pulls out an individual node — I usually identify it with a string. Crucially, because this is RxJS, you don’t get a snapshot back; you get an observable. So if that state ever changes in future, the observable fires and the view is notified automatically. That was my first mind-blowing moment with ngRx: every state variable in your app is now something you can listen to for changes, a bit like $scope.$watch being on by default. You render it in the view with the async pipe.
Changing state has more red tape, and that’s the point. You can’t write store.foo = 1 any more. There are three actors: the state, an action (usually a unique string identifying it), and a reducer — a pure function. The reducer takes the current state and the action, works out how to apply one to the other, makes a copy of the state, and returns it. Concretely: state is the number zero, the action is the string “add”, and the reducer says if the action is “add”, add one and return it.
Pure functions are the whole trick
A pure function is a term from functional programming: a function that does nothing other than read its arguments, return something, or call other pure functions. Anything else — even reading a global variable — is a side effect, and I’m sorry to say that if you haven’t been doing functional programming, you’ve been writing impure functions. The reason this matters: side effects are exactly what make testing hard. To test an impure increment that reads and mutates an outside count, you have to know its internals — that’s why Jasmine spies exist, and why we sweat through beforeEach trying to make the whole universe line up so the function passes. Testing pure functions is trivial: you pass arguments in, you assert on what comes back. For reducers, no spies, no setup — just inputs and outputs.
The counter, and time-travel
I pull the threads together with the simplest possible ngRx app: a counter with plus, minus and reset. The component injects the store and calls store.select to get an observable of the counter, displayed via the async pipe; the buttons dispatch increment, decrement and reset actions. The reducer is a handful of lines, and in the app module I wire up StoreModule.provideStore with the reducer, plus the store dev-tools module. That last one unlocks the best part: the Redux DevTools time-travel debugger. As you click around, it records every action and shows the state change — zero to one, and so on. You can replay the actions, and you can run “what if” scenarios: toggle a past action off and the tool reruns everything to show what the app would have looked like if it had never happened. That’s the easier-to-debug promise made real. There’s more I didn’t get to — immutable state, OnPush change detection making the app faster — but that’s a question for the AMA corner.