Managing State in Angular 2 Applications
Here is a video of the talk “managing state and Angular 2 applications” from the October 2016 St. Louis Angular lunch. The post below has roughly the same ideas, but with much less detail, in text form.
Managing State in Angular 2 Applications
The 6 stages of Angular 2 state management
Here at Oasis Digital in our Angular Boot Camp classes we meet developers working on Angular 2 projects at lots of companies, in addition to the projects we work on ourselves. As a result, we have a sense of the challenges faced while working with Angular 2 at scale.
The “at scale” part is important; we focus on serious, scaled use of Angular 2 and other tools; generally people building small things don’t have budget to take classes or engage consulting assistance. So take everything we write with a “grain of salt”: we are writing and thinking and talking and working on large complex projects.
Over time and across multiple projects, we have gone through a progression of how to think about and implement state management in Angular 2 applications, and our advice to customers is generally about moving along these stages.
What is state?
From a computer science point of view, state is any data that can change. The source of the change can vary, and the presentation of the change can vary. Regardless of those things, state leads to complexity, and in particular desired or accidental interaction between aspects of state is often, ultimately the greatest source of either value or cost in a complex software system.
Be wary of arguments that something is “not part of the state”; it often ends up part of the state as after all. For example, any of these often turn out to be part of the state that must be managed:
- URL / “route”
- Error conditions
- “Local” state
- Partially entered data
- Partially arrived data
- Reference / lookup data
Stage 0: State per component
Some small, simple programs have little meaningful state. The most obvious and easy place to store state in Angular 2 applications is in the components which “own” the state. For example, a component which displays a list of contacts retrieved from the server might simply:
- store that list of contacts in a field
- loop over the contacts with NgFor for display
- retrieve the contacts from an API using HTTP and subscribe.
In such simple cases, there is little to think about, and Angular change detection just works.
There are also slightly more complex programs in which the state can easily reside in a handful of independent components. If the components don’t interact in any way, even if each one has a small amount of state the overall state full complexity of the software remains very low.
We see very few programs “in the wild” that remain in this stage of simplicity.
Tip: Watch the video instead of just reading. The video has diagrams.
Stage 1: State in interacting components
Slightly more complex applications have some interaction between components. The most straightforward and obvious way to handle this interaction is to have each component “own” a portion of the overall application state. Then use events and bindings to push that state up and down and across the component hierarchy to other components which need to receive it.
This design starts straightforward, and continues to match what new developers are shown in the documentation, in the QuickStart, and so on. This stage of complexity is what we most often see developers begin to create as they learn Angular 2. Depending on the complexity of interactions, some applications can get quite far with this design.
Unfortunately, the complexity and difficulty can begin to increase depending on the details of the interactions of these various state full components. In particular, things get painful when you realize there are many copies of various aspects of the state of your systems spread throughout a component hierarchy – and you have lost track of exactly which component “owns” which aspect of the state.
With increasing fury at the keyboard, it is possible to keep the relevant parts of the state in sync with each other, sometimes resorting to awful hacks:
- ngOnChanges methods which implement business logic, pushing state back and forth between components
- Even worse, set timeouts and other means to notice data has changed in one place and needs to be copied another place
- Begging or raging on StackOverflow for something like $scope.watch() from Angular 1
Still, it’s important not to be overly negative about this approach. For applications with only a little state complexity, it can work fine. Few of the applications we build that Oasis Digital remain in such a condition though.
Stage 2: State in one component at the top
In a quest to bring order to the chaos that occurs with different bits of state owned by different components spread across a hierarchy, a wise developer will study the Angular 2 documentation and learn the key organizing principle:
Bind data downward, emit change in events upward
To implement that, move state ownership upward through the component hierarchy, such that each piece of state is owned at a high enough level that it can be pushed down to any other components that need it. In extreme cases, some applications have essentially all of their state all the way in a top-level component.
This fixes the “sync” problem, and is very compatible with Angular. In particular, it enables many of angular’s key performance optimizations, it lets you specify change detection as OnPush.
However, with a more scale the problems appear:
- Topmost or other high-level components and up looking a lot a bin of global variables
- Extensive code throughout the component hierarchy, a “bucket brigade” carrying numerous events upward and numerous aspects of data downward.
- As of October 2016 (with comments by core team members about a fix coming), the numerous event and data bindings are all completely untyped, outside the realm of where TypeScript can detect and assist with them.
We got quite far on applications with this approach. It can handle applications of modest complexity with no trouble at all, and it is very compatible with the Angular binding/event view of the world. Ultimately though, the problems noted above became unworkable and we have ceased using or recommending this approach.
Stage 3: State in services
In a project where the bucket brigade is out of control, developers will often switch to a common technique from Angular 1: put the primary representation of each aspect of the state of the software, in services which are then injected wherever they are needed.
To do this, you must set aside OnPush, and rely on Angular 2 change detection. That change detection is surprisingly efficient, so this compromise is not particularly problematic in many applications. Moreover, each state full service can be injected only and exactly into the components where that state is needed. The bucket brigade is gone, and instead the dependency structure of the source code maps to the use of state. The software becomes much easier to reason about.
Unfortunately, reacting to change in the state is still quite difficult with this approach. That can become obvious when the project starts using the following hacks:
- Create a component which injects services and then binds data from those services using its template, into a function. Write code in that function to perform computation based on that joint state changing.
- Create a component which injects services and then binds the data from those services into another component underneath it; inside that lower component, use OnPush for efficiency and then write business logic in ngOnChanges to be notified and take action when the data has changed.
Why are these hacks? We consider them hacks because they abuse Angular capabilities primarily intended for manipulating the view/UI of an application, to instead call business logic. If you find yourself writing “business logic” in ngOnChanges, things have gone horribly awry.
Even with the hacks in place, with a tiny bit more complexity, getting programmatic control over the changes in state of the system becomes very difficult and tangled.
Stage 4: State in Observables (in services)
- Remove/disallow state in components
- Remove/disallow (most) state in service class fields
- Put the state inside Observables (sometimes Subjects or BehaviourSubjects actually) in those service classes instead
- Inject the services to whichever components need them
- Use the async pipe in the components to get the data from the observables into the view
- Write code in the services which uses the RxJS API to respond to and propagate changes to state stored in these Observables
This general pattern has already been reinvented numerous times in the Angular 2 world. It has many advantages:
- Easily bring state to where it is needed
- Single copy of each piece of state
- Clear obvious place to write reactive business logic
- Extensive selection of RxJS operators to manipulate the state
- Efficient use of Angular 2 view binding with OnPush
An observable centric, application-specific state management mechanism can work very well. At this stage really the only downsides are:
- Each team or application reinvents a way to do this, and therefore is not benefiting from any common libraries or tooling.
- A large complex state spread across many observables becomes unwieldy to the extent the front aspects of that state interact.
Also at this stage, developers generally have the feeling that they should have seen this problem before. And in fact many developers have, and already worked on solutions:
Stage 5: Choose and Use a State Library
We consider this the stage that any nearly every mature Angular applications to reach. Choose and use a proven library or approach for state management.
State library options
The two libraries that come to mind most often are those which implement the Elm architecture / Redux pattern with Angular integration.
- NgRx/Store and friends
Regardless of which you choose, you obtain generally the same benefits:
- Your ideas and code are useful across Angular and other, non-Angular platforms
- Write mostly unencumbered TypeScript code, rather than code which only makes sense and executes meaningfully with the help of a library
- Excellent control of change
- Excellent test-ability – in most cases the essential logic of your application can be tested apart from Angular itself
- Tooling support, for things like “time travel debugging”
- Community – when you have a problem, you’re likely not the first to have this problem, and you will likely find hopeful and useful discussion online
Beyond the libraries
Alternatively, you might find that the Elm/Redux approach is not ideal for your needs. Here are some other directions to consider.
- Over in the React world, MobX is receiving substantial attention as a less tedious way to obtain most of the same benefits as Redux. Perhaps it will find its way here to Angular.
- If you store your state in Firebase, Firebase itself will handle propagating the state around your application (as well as between numerous devices). In some it can serve most of the needs for state management.
- The State Action Model pattern (also see some code samples) contains many of the same ideas, and appears more similar to MobX than Store or Redux.
- Andre Staltz, author of CycleJS, argues that Observables-all-the-way-through is compelling. Even if you never use his CycleJS library, watch a few talks and take it for a spin.
- For some applications we are loading data with GraphQL; although are currently doing so in the context of the state management systems, the opportunity is out there for certain aspects of local state management to be abstracted away. The future around this is still unfolding, but it seems inevitable that this new direction of abstraction will strongly affect application state management in the future.
How big before this matters?
Now you might think from this description but I’m talking only about huge complex enterprise apps. Not true. Even fairly small applications, can end up having surprising difficulty in state management. We have a piece of training curriculum which attempts to manage state responsibly in a tiny application whose code can be fully reviewed in just a few minutes. It ends up at stage 4 (in the list above) without really trying.
You might also think, “but I’m not talking about application state, I’m just talking about whether a checkbox is checked on a form”. This sort of thing initially seems like it can be omitted from a broad notion of application state, and that is true, until you want to implement certain features, and then it is not true anymore.
Most broadly, if you are not working on an application complex enough to care about state management, why are you using a library as large, complex, feature full, and powerful as Angular?
In our work at Oasis Digital, we have concluded that for most projects, the right answer is to proceed directly to full powered state management.
An alternative view
Our point of view here may be contentious, and is certainly not the only point of view among experienced Angular developers. Most notably, Ward Bell, and all-around experienced Angular guru and key author of the Angular official documentation, argues that only a small minority of Angular applications warrant a complex state management approach.
One wish for Angular 3: Stateless components
Currently, components and Angular 2 are classes, classes are a deeply OO concept which mix behavior and state. For some uses, this is excellent. For others, and for some of the architectures described here, the tight coupling between stateful components and the Angular 2 view mechanism is not so beneficial.
A second wish: Higher-order components
If we had stateless components, that gets us halfway to tooling support for rigorous separation of state and view. How do we get the other half of the way? We get there using higher order components, you could think of these as components that emit components, meta-components, functions that emit components, something like that.
The point being that sometimes you want to specify all the gory details of the component, and the current Angular decorator mechanism is perfect for that. Other times you want to programmatically say, “please wrap my component with another component defined by the following function”. There is not currently a way to do that. There are technical challenges with it, around how Angular compiles components statically.
However, I have great confidence that the core Angular team will eventually (Angular 3/4/5/6/N) will grow something akin to higher order components.
If you are working on nontrivial Angular applications, as soon as you hit state management difficulty start learning about sophisticated, powerful state management approaches and tools ASAP.