Skip to main content

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.

Building a Complex App with Angular2: Lessons Learned

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.

Overview

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:

Language (TypeScript)

For purists, this may blur the line on what is “true” JavaScript, but in our experience, it stays close to the spec, delivers new features early (like async/await), and having type-checking and good intellisense available makes for a very productive language to use for JavaScript programming.

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)

Angular2 embraces reactive programming, and so should you. It’s a whole paradigm that is a great fit for callbacks and asynchronous programming, and Observables are core concepts that you will use frequently (whether you know it or not). While there is a large body of work including specific languages and libraries that fall under the “reactive” umbrella, the relevant incarnation here is RxJS and version 5 has been stabilized recently with feedback from its use by the Angular team. Andre Staltz has a good primer on Reactive programming and JavaScript Jabber has a podcast with Matthew Podwysocki with background and details.

StateManagement (NgRx)

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.

ModuleBundler (Webpack)

Ultimately, every serious JavaScript application will use some module loader/bundler, and we used Webpack. Most developers using Webpack have a love-hate relationship with it because it is incredibly powerful and flexible, but also has obscure documentation and unusual syntax that makes its power and flexibility difficult to harness. Version 2 is officially out and streamlining some of the pain points, and the documentation seems to be improving. However, the AngularCLI being developed by the Angular team wraps/hides Webpack so developers shouldn’t need to manually configure it anymore. Ideally, when using Angular CLI, it should “just work”; this hasn’t always been my experience, but it is getting better as this tool chain progresses.

Testing (Jasmine/Protractor/Karma)

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

TypeScript

Strictly speaking, TypeScript is a language which is a super-set of JavaScript language – meaning it supports everything that JavaScript supports but also supports additional language features that JavaScript does not. Those additional language constructs ultimately get converted back into normal JavaScript through a process called transcompilation or “transpiling”. The folks behind TypeScript have found innovative ways to support advanced features in TypeScript which don’t exist natively in JavaScript but can be transpiled into JavaScript so you get the benefit of using those advanced design-time constructs that still run natively in today’s browsers. It’s a sneaky way to push the boundaries with new techniques without requiring your users to upgrade anything on their side. Some of these new features include async/await, which allows developers to write code that is asynchronous in nature, but has the visual simplicity of synchronous code.

One of the other key benefits of TypeScript is static type checking which can find errors in your source code prior to runtime. Mostly gone are the notoriously painful and hard to find errors in JavaScript where you left out a comma or some other simple syntax goof which leaves your code in a state which runs fine until it barfs, but you cannot understand why it behaves that way until that palm-meets-forehead moment when you finally realize the error. The static typing of the language is especially beneficial when you have a large codebase and the team grows beyond a couple folks. Having explicit types for your models in a project brings clarity to the application functionality, and ensures a common definition for your application data structures. As much as JavaScript is loved for being a dynamic language, having static typing and clearly defined classes improves the maintainability of a codebase immensely.

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.

Dependency Injection

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.

RxJS

Reactive programming is being discovered by many through several popular FRP JavaScript frameworks (ReactJS, Elm, CycleJS, etc.), but as mentioned in the overview, has a history that predates this relatively recent surge of interest. While one can find theoretical explanations in the Reactive Manifesto or the Microsoft Rx site, or visual explanations using reactive animation, many developers struggle to get an initial understanding of the essence of reactive programming. In an effort to provide a basic foundation, I would highlight a couple key points…

First, the essence of reactive is that you are reacting or responding to some change in the universe. Each change happens as a discrete event that is consumed (non-destructively) by listeners that are interested in those specific changes. You’ll often read about streams or sequences – these are just the collection of the same types of changes that occur over time – which are simply a way to keep things organized. Anything that produces these change events is considered an Observable. Observables are things for which you can watch, on which specific changes will occur and provide you an opportunity to do something. You typically declare your interest in specific types of changes by subscribing to an Observable, at which time you provide a function that you want to be executed on each instance of a change event. Keep in mind, you are declaring your behavior, but it will not execute until a change actually happens. Basically, you are providing a callback function to be executed whenever a change occurs. It is within this callback function that you put whatever logic you want executed when something happens. From my perspective, this is one of the reasons that JavaScript is a good fit for reactive programming -- functions are a first-class primitive in the language, and most JavaScript developers are already familiar with thinking in terms of callback functions.

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!

The Angular team has embraced reactive extensions for JavaScript (RxJS) and explicitly use an Observable for the HttpService. In practice, what this means is that when you make an HttpService call, you effectively subscribe to a change event that comes into existence when you receive the response. What you get back from the .get() or .post() method is an Observable, on which you must call the .subscribe() method and pass in a success callback function in order to get access to the response data; you should also provide an error callback function as well in order to handle any error scenarios. One key advantage of using Observables here is that it makes a non-blocking asynchronous http call so your application is more responsive and performant since it can do other work while waiting for the response to come back from the server. The other key advantage is that being an Observable, there are a slew of other methods that can be used to “manage” this response. I won’t go into them here, but Jay Phelps has a good talk about the complex scenarios that can be easily handled via the various operators available for RxJS Observables. Before we talk more about reactive programming, let’s discuss the value of using NgRx for state management and come back to this topic.

NgRx

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?!

Strictly speaking, the Action dispatches can be performed anytime, but typically they occur at the end of your logic when handling actions. With some basic organization within your application, this naturally creates a Model-View-Controller pattern that is very easy to understand. As good JavaScript code is considered short (ideally, visible for a developer on a single screen in a reasonable font size without scrolling) and has a single purpose, most likely you will break out the actions within your UI components into separate classes with methods that accomplish the goal, in order to make your actions reusable across various components and keep your components smaller (if you aren’t doing this, you should be). Let’s call these classes that perform actions “action handlers”. You can also think of them as “Action creators” in the Action sense, because the result of processing an action (service) will likely be to create and dispatch an Action (object) to update state. Within these action handlers, we can perform front-end validation, then call our api layer, then process the response from the api, and ultimately call Store.dispatch(…) to modify our application state based on the result of the action. Using this reactive programming approach with RxJS, your code stays relatively well-structured. Your action handlers are the controller, the Store is the model, and the Component is the view. If your Angular application uses a microservices-style backend, the frontend tends to have lots of methods/actions to call apis, but each method/action is typically very small and focused, which lends itself to being very clear and maintainable over time.

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.

Webpack

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. 

Given the size and complexity of modern web applications, modularity becomes critical – the ability to organize chunks of JavaScript code to maximize reusability, cohesion, and composability while minimizing redundancy. Addy Osmani has a good article on modularity in JavaScript. Although slightly dated now, it is still informative. Several different approaches have existed in the JavaScript ecosystem (AMD, SystemJS, RequireJS, CommonJS, etc.) but with ES2015 the standard approach is to package code as modules using “export” and “import”. This aligns nicely with TypeScript where you can export a class with specific behavior through public/private methods and then can be imported by anyone else who needs that behavior. This modularity keeps a large codebase sensible and provides reusability, and Webpack bundles all of your modules so that you effectively have one (or several, if desired) bundled JavaScript file for simplicity at deployment and runtime. 

Given Webpack’s role in the application of managing your various modularized JavaScript files, it has developed some additional features that web applications find quite useful for keeping your JavaScript bundles as small as possible. One of which is “tree-shaking”, which refers to the ability to prune out modules which may exist in your code base but are never actually used or “import”-ed. Webpack can identify these and exclude them from your final JavaScript bundle, thereby keeping it as lean as possible. 

Additional steps can be taken to make the JavaScript bundle more compact without affecting the functionality, often at the expense of readability. Variables and method parameter names can be changed to very short values, often single-letter names. Whitespace such as newlines or tabs within the code can be removed. Comments can be stripped out entirely. These individual steps may be referred to as “mangling”, “bundling”, or “minifying” but collectively the process has come to be called “uglifying” because it makes the code less pretty to look at. In the end, however, it can make a significant impact on the size of the JavaScript bundle. Ultimately, a smaller bundle means faster page-loading times, less bandwidth consumption, less memory usage, and potentially faster execution times.

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.

Testing

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!

Solution Category:
Speciality Category: