Building maintainable Angular applications
We’ve been using Angular since beta 16. After putting a few projects into production, we’ve come up with a set of ground rules to help build applications with Angular.
Keep Components Tiny
Building small components is easy at the start of a project, but preventing them from growing takes discipline. During the development of our first Angular application at Versett, we continuously added functionality to our components rather than splitting them out into new components. Our growing files eventually forced us to rewrite significant portions of our codebase. Our mistakes led to coupling between unrelated pieces and made future modifications dangerous due to the fragile nature of the code.
Since then, we have implemented a policy to keep components small and subsequently, they should receive only the data they need to complete their job. In turn, we find that our components are much more reusable and manageable as our applications grow.
Presentational and Container Components
One similarity between React, or other component based libraries, and Angular is presentational and container components–the idea of splitting the view and the logic into their own separate components instead of combining them into one. Container components consist of the logic for the presentational components and pass through data that is used for rendering. This pattern helps ensure that the presentational components get only the data that they need and allows them to be more generic and reusable. Additionally, through the use of transclusion, you can avoid nesting throughout your presentational components, which helps to keep them organized and decoupled.
The ngrx example-app is a great example of how containers should look versus how a presentational component should look. You’ll notice the containers primarily handle which components to render, versus the presentational components which receive inputs and render it on the page. Instead of performing logic within the presentational component, the presenters use the @Output() feature of Angular to let the container decide how to handle events.
Dan Abramov has an excellent article on presentational and container components, which refers to React, but the same principles apply to Angular.
Testing is essential for long term maintainability. We’re not perfect testers at Versett, but we believe that if we write modular chunks of testable code, it will age more gracefully than if we didn’t. In our initial Angular projects we made the mistake of not testing, which led to regressions making their way into production on more than one occasion. We now ensure all our code is thoroughly tested and you can find .spec.ts files throughout our codebase.
Angular was built to be tested. It includes a large suite of testing tools and has dependency injection baked into the core of the framework. Dependency injection makes mocking out services, pipes, directives, and components incredibly easy. Keeping your components tiny, dependency injection, and mocking helps to ensure that we precisely test small chunks of code for more useful test cases. The Angular testing guide is a great place to start for writing your first test.
The Angular team jumped in head first with RxJS and reactive programming. Embracing RxJS can improve code maintainability dramatically by allowing manipulation of not only data, but how data is retrieved. If you’re one of the many struggling with RxJS, observables, or subscriptions, here are a few tips:
- Use the async pipe and avoid using .subscribe().
The biggest mistake we made when dealing with observables was trying to subscribe to observables in order to extract the data. Subscribing led quickly to coupling, as well as a very confusing flow of variables throughout our components and templates. The async pipe allows data to be streamed directly into a component, which lets us get rid of those messy subscriptions. If we find an instance of subscribe() during a code review, we will try to find a way to restructure our code without the subscription because it often leads to cleaner and more understandable components.
There are lots of reasonable cases for using subscribe(), but we believe that overuse is considered a bad code smell.
- Tiny components allows the async pipe to do the heavy lifting.
RxJS operators allow us to manipulate streams to contain only the data we need for our components. Rather than having to capture and manipulate our internal state every time it changes with ngOnChanges(), formatting data within a stream prevents us from having to deal with change detection and passes the data directly where we need it to be.
- Only deal with observables in containers, never in presentational components.
Following the pattern of using presentational and container components, we find that we rarely need to subscribe to a stream in our containers and never need access to the observables in presentational components. The separation of streams and data is a very effective way to remove coupling between the presentational and container components.
- Give ngrx a shot.
I’m skipping ahead to state management, but ngrx ties directly into RxJS and we were forced to learn the principles of RxJS in order to effectively use ngrx stores. Sometimes the best way to learn is by jumping into the deep end.
There’s only one response from the server, why would we treat it as a stream?
Our whole team disliked observables when we started with Angular, claiming that promises made more sense for ajax requests. It was only after we had added ngrx to our projects and embraced the pattern that we understood the original motivations of the Angular team, and how RxJS could help keep our code maintainable. If you don’t believe me, Christoph Burgdorf has an article about observables which explains their benefits, better than I ever could.
Knowing when to add state management is a tricky problem to solve. Often using component state is enough, but knowing when to add it to a growing application can be hard to identify. While we didn’t realize it at the time, the root of many of our problems was a lack of state management early on in our development cycle.
If you’re new to Flux-style state management, you can check out a guide on getting started in Flux. There are several flavours of Flux-inspired tools such as Redux, MobX, and Relay, but for our projects, we opt for ngrx. Ngrx is a Redux inspired, Angular focused, and RxJS-embracing state management library. Backed by several of the Angular core team members, ngrx does more than just manage state: they also have libraries for managing side effects of actions, integrations with the router, and even handling web notifications. You can read an introductory video for ngrx for a more thorough understanding of what makes it different.
Versett is a product design and engineering studio. If you like this post, you’d love working with us. See where you’d fit in!