Building a Complex App with Angular2: Lessons Learned
Our dev team recently completed a large Angular2 application, and in the interest of sharing our experiences, this article is intended to share what went well, what didn’t, and where the strengths of Angular2 really shine.
Our application is ~50K lines of code and contains ~200 UI components to support a business application for constructing and pricing customer quotes for sales opportunities for a Fortune 500 company with a large array of products. The scope of the project has shown that the new Angular framework is more than capable of providing a great user experience in a component-based UI using tools that make for a well-organized and maintainable codebase for a large application. This article is intended for software developers and managers who may be considering (or already are) using Angular2+ for their front end and want to get an idea of what factors may contribute to the success or failure of a project; this is not a detailed technical discussion of technical merits of Angular2 or its libraries, as there are plenty of resources out there for that need.
First, it’s fair to say that the label “Angular2” encompasses a variety of tools, techniques, libraries, etc., and it may be intimidating to teams who underestimate the scope of what they should be fluent with to be successful. Especially for teams coming from Angular1, there are enough differences that success is not ensured simply because you have used Angular1 in the past. Yakov Fain wrote an excellent post highlighting the different tools/knowledge sets related to Angular2, as well as grading the level of difficulty for each. Rather than rehash it here, I recommend reading it as its easily digestible and a useful analogy. Some key technologies that stood out for our team are as follows:
Dependency Injection (Angular)
I’ve never met a DI framework that I really liked...until now. It’s built into the Angular2 framework, so you don’t need to add anything to use it. In fact, that’s the real beauty of it – you don’t even notice it’s there; it simply does its job and gets out of your way! While not a specific library like the other items mentioned above, this is significant enough to call out as a key feature.
Reactive programming (RxJS)
Application state management is always a challenge for complex applications, but Redux has shown the way forward and NgRx is basically an Angular implementation of the Redux functionality. It makes it very easy to create and update the data store to manage application state; then simply use RxJS in your components to subscribe to the state in which you are interested. The implication of this approach naturally creates a Model-View-Controller architectural pattern in the front-end code, which we will discuss shortly.
The Angular team has put a lot of thought and effort into ensuring UI components are testable and that the Angular framework provides what you need to do that testing. Towards that end, test harnesses for both unit-testing (Karma) and end-to-end testing (Protractor) are easily incorporated into a project, using Jasmine to define the actual test cases. Once set up, tests should be easily run from the command line using an npm script target, and allow developers to incorporate additional tests as new components are built.
The above list provides the key tools/technologies that we used in our project. Let’s discuss them in more detail in regards to the impact they had on the project.
Individual Topic Deep Dive
Static typing facilitates the tooling around the language as well. Many of our team members use Visual Studio Code which has excellent integration for TypeScript features (as you would expect, both being from Microsoft). It has improved significantly since it first came out and has so far managed to keep its lightweight text-editor feel, while adding more and more functionality through extensions. In my experience, the integrated terminal provides a complete cohesive environment for developing applications when using the text editor / command-line approach.
The way Angular2 has implemented Dependency Injection works well. So well, in fact, that you forget it’s even there. Once you make the NgModule aware of your classes for your app, the framework will instantiate them for you based on class (type) names. You simply specify in a component’s constructor the various dependencies for a component by declaring each dependency as a parameter to the component’s constructor with a name & type. When Angular instantiates that component, it will provide an instance of the class to the constructor to satisfy each dependency for that component. Because that constructor property is a class property (thanks to some TypeScript magic), this property/dependency is available for use in any methods within the component without any additional declarations or configuration. Hence with minimal clutter, dependencies are declared and satisfied at runtime by the Angular framework. Quite simple, elegant, and effective.
Second, a key aspect of reactive programming is composition -- layering your change handlers together in a way that provides complex behavior from very simple rules. For example, if you have shopping cart functionality in your application, and a user changes the quantity on a line item, you will want to update the individual line item total, as well as the grand total on the order. Rather than trying to make all of those changes at once based on all the possible changes that might affect any of those values, with reactive programming, you would define your change handlers such that the line item total is the product of the line item quantity and unit price; separately, you would define your grand total as the sum of each individual line item total for all items in the order. When the user changes the quantity for a line item, it triggers the update of the line item total (because you are reacting to a change that you declared is relevant). Subsequently, the change of the line item total triggers the grand total to be recalculated. Each change is very specific and easy to understand and express, yet collectively very complex application behavior can be achieved. Additionally, a key feature of composition is that you are only reacting to the things that you really care about. Reacting to one change may trigger the creation of another change. Alternatively, there may be changes that you ignore, because you don’t care about reacting to those changes. Effectively, this becomes asynchronous message passing where changes cascade through the system, affecting anything (and ONLY those things) when it’s something you care about.
What may be implicit in the discussion thus far, is that this approach is especially effective when you can frame the problem or behavior of an application in a way that aligns with a reactive perspective. If the values being calculated are derived from other values, reactive is a good fit. If your user interface is derived from actions that user takes within your application, reactive is a good fit. Like every other paradigm or trend that has ever occurred in software development, though, reactive programming is not a panacea. It’s not the right solution for every problem, and it should not be used for everything. It is a paradigm that offers the ability to manage certain types of complexity very effectively. There is a learning curve, especially when it comes to changing the way you think about a problem. Having said that, if your problem really is a certain type of nail, reactive programming is a very powerful hammer!
As mentioned earlier, NgRx is RxJS powered state management for Angular applications, inspired by Redux (literally, that is the tagline in GitHub). For those not familiar with Redux, a short explanation would be that it is a state management framework which simply allows a developer to define “reducer” functions for any data which makes up application state; these reducer functions take as parameters the current state and an action, then returning the subsequent state. Reducers are “pure” functions and must only determine the next state by using the two inputs (current state & action); the action object contains an ActionType property which uniquely identifies the type of change that has happened which should affect state, and a Payload property which can be whatever data is associated with the Action. These two things get passed into the reducer function and the output of the reducer function becomes the new value of current state. The NgRx framework takes care of the basic plumbing around this process so you don’t have to. All you do is define one or more reducer functions to process whatever state changes you need to support when actions happen. This collection of functions taken in their entirety make up your application state management. You simply decide exactly what functions you need based on what state data you need to manage. With NgRx, these reducer functions are each properties of a single object (the “Store”), and the NgRx libraries provide the wiring to initialize and manage the functions so that you only need to provide the implementation of whatever reducer functions you create to keep track of your state data. Then you simply call the Store.dispatch(…) method, passing in the action object to produce the next iteration of state (internally, the NgRx framework will inject the current state into this call so it is present when the actual reducer function is invoked).
One key point to keep in mind is the separation of state changes from business validations. To help illustrate this point, let’s clarify the difference between an action (think of it as a “logical service” on a controller – the stuff you want to do in response to a business/real-world decision or event) and an Action (an object which has properties of ActionType and Payload, and is dispatched to affect a change in state). You will want to keep the logic of your state changes minimal, and this means doing validations, etc., before generating (a.k.a. “dispatching”) an Action to process a state change. You only want to process a state change when you know exactly what state change to process. This typically means doing validations and making your backend calls and then processing the appropriate state changes in light of the validations and backend responses. For example, the application state you need to update may be a list of error messages that are currently valid based on the user’s most recent attempt to make a change to the application data. Alternatively, the application state may need to be updated to reflect new values if the backend calls succeeded to ensure that all views in the front end render the correct information now that a change has successfully been processed by the backend. If you think of your “store” as the universal source of truth for your application, you only want to update it when you have a specific change that you know is valid to make. Hopefully, this makes it clear that while your application will have actions to support different services/features within the app, you typically only dispatch an Action at the tail end of an action when you know exactly what state changes are needed based on the result of whatever steps have occurred within the action. Make sense?!
RxJS & NgRx
I feel it’s worth calling out the pairing of RxJS and NgRx because it provides a very powerful combination for modern web applications. If you recall our discussion of RxJS, we discussed the importance of using reactive programming when it’s a good fit. We said that reactive programming is a good fit when you can describe the problem in a way as one thing being derived from another. Well, if you think of your user interface as being derived from your application state, reactive is a perfect fit. Not only does this perspective align with the “reactive” nature of RxJS and NgRx, but it naturally encourages a separation of the logical state of your application from the visual rendering of the presentation layer based on the application state. If we can define our presentation layer as a derivation of our logical application state, then processing changes in our application becomes much easier – we just need to update our logical application state (without worrying about presentation issues), then separately ensure our presentation layer renders correctly based on the application state. Dividing these realms eases the burden on developers – application state doesn’t depend exactly which screen we are on, it’s our universal source of truth; and when we are rendering a presentational component, we have full access to the universal source of truth (application state) but only need to worry about the state information applicable to what is relevant to our rendering.
If there are any teams out there who are not using some form of state management facility, you are making things much more difficult for your developers than necessary. It is very easy for developers to reason about the logical application state, given some changes in the application. It is also very easy for developers to reason how to render a UI component given specific value(s) of application state. This simplifies developing, testing, and debugging, and overall makes the application more maintainable in the long run. Don’t underestimate the value this brings to your team and an application. Now that I’ve used these tools, I cannot imagine building an Angular application without using NgRx or some comparable form of state management. Don’t waste your time – start using NgRx today.
As a final note on this topic, I want to make clear that the benefits of RxJS & NgRx are not exclusive to the Angular world. In fact, any developer who has worked with React and/or Redux already understands (hopefully) what I described above, and it’s a cornerstone of most functional reactive programming (FRP) web frameworks. One of the key benefits that Dan Abramov highlights about Redux is the ability to reason about the state of an application and how code will behave during changes. In large part, this ability to reason comes about from separating the concerns of logical application state and the rendering of a component, given a specific application state.
As mentioned above, our team had a love-hate relationship with Webpack, though more of the former as the project went on. This tool is one that when its working properly, stays out of your way and you don’t really notice it.
On a good day, Webpack does all of these things for you, and you hardly notice. It just works...until it doesn’t. Then you poke through lots of strange plugins, loaders, and options in your webpack config file until you find some arcane combination of settings that suddenly make it work again. Then you realize how much you depend on it and pray that it keeps running for another few months because you don’t know what you’d do without it! More seriously though, the recent enhancements to Webpack 2 have done much to provide sane defaults and keep the toolchain working well with minimal effort.
One thing we would do differently on the next project is to wire up the testing tools in the beginning of the project. When we started this project, Angular2 itself was still in beta, and the testing capabilities were less polished. In a desire to make tangible progress early, we made the decision to move forward with minimal testing (unit or e2e), and only added automated tests later at the end of the project. At that point, the official angular 2.0.0 code was released and in fact already had several point releases and the testing capabilities had improved and were easily configured and incorporated into the project. Had our tests existed earlier, it certainly would have helped identify when code changes in either the front-end or the back-end caused things to break, and would have saved a bit of time figuring out why stuff that worked yesterday stopped working today. But we dealt with the reality we were facing, and revisited testing after the tools became more stable.
Ultimately, we did institute a Unit-test framework and we used the Karma test runner with tests written in Jasmine. The tests are executable via an npm script and run quickly with a good coverage summary at the end. I’m sure as more tests are added, it will slow down the overall duration, but this seems to be a common choice for web applications. We are happy with this base, and may incorporate more tests soon. At the moment, we only have a few unit test cases, because we haven’t put the energy into mocking all the services that power the app, which are needed to satisfy the dependencies in our components for testing purposes. It’s a testament to the strength of component-based UI architecture and Angular that we could stay ahead of as much change as we did without more regressions while having limited unit tests. Our component tree made sense to the team and isolated different concerns and state so effectively that it was typically easy to know where changes were needed, and our changes rarely broke existing functionality. The biggest source of regressions was changes in the backend API which weren’t communicated to frontend developers who needed to make corresponding changes, but that is a different problem to solve!
For end-to-end (e2e) testing, we used Protractor. This is an easy choice, as it essentially comes from the Angular team and is the blessed e2e testing tool for Angular apps. This wraps several other tools and makes them easy to use, specifically Selenium WebDriver/Server and Jasmine. Given that the tests themselves are written in the same language/toolkit (Jasmine) as our unit tests, that helps with consistency and ease of use. The fact that the e2e tests are exercising the actual browser experience is a big plus, as we can be certain that this better represents the user’s experience with the application. One upside to this is that we found it very entertaining to watch the test execution as it steps through the app while running the tests. One downside to this however is that the tests themselves are sensitive to timeouts and particularly sensitive to the selection criteria used to identify elements being validated, but that is a common challenge for any automated testing tool.
As both these test harnesses (unit and e2e) were put in place only later in the project, we decided to focus our energy on the e2e tests since we felt we got more bang for our buck, given that it is exercising the actual browser and effectively testing the API backend as well. Once we settled on the approach and test settings that worked for our application, we rapidly developed additional test cases and quickly covered a majority of the application. Like the unit tests, the e2e tests can also be triggered via an npm script and have become a key tool in our process to ensure ongoing changes don’t break existing functionality.
Overall, our experience with Angular2 was very positive. Our team had a mix of developer experience – some developers had no Angular experience at all, some knew a bit of Angular1, some knew ReactJS but not Angular2/RxJS, some had worked with Ionic2 which had given them exposure to Angular2. None of us had really worked with Angular2 prior to the project (especially given how new it was, and the fact that we started the project in May 2016). Ultimately, we grew with Angular2 through its beta and rc releases, and saw it become a solid, stable platform, upon which we delivered a successful solution. Now that it’s been in production for a few months, some of the user feedback we have heard is that it’s “one of the best tools used for managing our business creating quotes in many, many years" and that it’s very intuitive. Certainly, some of the credit goes to our awesome UI/UX Design team that came up with the vision for this app, but credit to Angular2 for allowing our application development team to realize that vision quickly and effectively using this framework. Well done, Angular team!