At work we have been working on rewriting the frontend of our large web application from scratch using Angular 2 (now Angular 4). In this post I will be detailing our app architecture and the experiences we have had so far.
Previously we used AngularJS which served us well but really slowed down on heavy pages with a lot of elements. The sheer number of digest cycles and re-rendering was becoming a real problem, and to add to that we had increasing numbers of people expressing the opinion that the front end had too much going on, was too complex, and so on.
About a year ago we had 2 team members spend a 2 week long sprint prototying a very limited application - I built one with the then beta release of Angular 2 and one of my colleagues built a prototype with React. We both used the Redux data flow. After comparing the two performance wise and having a few team sessions discussing the benefits and disadvantages of each patch, we eventually decided to go with Angular. The performance of both applications was pretty much equivalent according to Chrome dev tools. Most of us were open to either approach and thankfully there were no bitter disagreements amongst ourselves :)
Fast-forward a year to the current day and we have just released an MVP for internal feedback and testing. Below are my thoughts and good/bad experiences so far during the building of this application.
Before starting, I just want to make it clear I won't be providing any comparisons with React, as I have minimal exposure to React personally - just some tutorials and online course stuff. I've never built a large application with React and so can't comment on it much.
There's also something else I need to mention - we have a JS library we started building before the beginning of this new front end application. This JS library acts as an abstraction layer for several of our other smaller front end applications. It handles user authentication, communication with our API and data caching - removing some of that responsibility from consuming applications and allowing them to focus more on their own specific functionality.
This library was very helpful in that regard. In my opinion it has brought a lot of complications into our new project. This JS library is object oriented, providing a collection of classes and helper functions that can be used for data fetching, manipulation and saving to the server. Despite the the auth/API abstraction meaning our app doesn't have to worry about implementing this functionality (a very good thing), its OOP architecture has caused us problems in the immutability and change detection side of our application which we've had to do our best to try and work around. I've tried to not let this unfairly influence my overall thoughts on building applications with the new Angular framework.
One of the biggest changes for me was the switch to TypeScript, the native language of Angular 2. At first I wasn't that excited about it; but as I've learnt more and more by using it each workday I've really come to like it. Having type information available for IDE autocompletion and type compliance checking is really cool. It makes it harder to accidentally provide an invalid value to a function or assign an invalid value to a property - something simple like a one character typo in a string for example can be picked up by the IDE. Without this checking, such errors can be hard to debug if you don't immediately notice the typo in your code. Thankfully the use of TypeScript is helps to eliminate many of these cases.
Another benefit is that we get to use ES6 features with TypeScript as well - all the awesome things like let and const, rest/spread syntax (...), destructuring, string interpolation, arrow functions, default parameters, and more. We specify in our tsconfig to compile down to ES5 so older browsers can understand our compiled code.
You won't get IDE autocompletion for JS libraries that do not provide TypeScript Definition files however. But based on my experience so far I've found that the benefits of using TypeScript outweight the disadvantages.
When you are new to Angular 2, the template syntax looks really strange compared to AngularJS. For example
structural directives use syntax such as *ngFor
, one-way bindings look like
<img [src]="getImageSrc()">
, event bindings look like <input (change)="onUsernameChange()">
,
and two-way bindings used with the familiar ngModel directive look like <input [(ngModel)]="username">
. The
latter is just a combination of the one-way binding (it displays the value from the component instance) and an event binding
(when the input value changes, the value in the component instance gets updated). The way to remember which braces come first
(from the Angular docs) think "banana in a box" - i.e. square brackes on the outside, curved brackets inside.
You may be thinking this is ugly syntax! You wouldn't be alone there. An important note is that the templates are compiled by the Angular compiler and the actual HTML displayed by the browser looks much more "normal". I'm not opposed to the syntax myself - I guess I've used it so much I don't mind it. I actually like the Angular 2 syntax more than the AngularJS syntax even though it's a greater deviation from traditional HTML.
One reason in particular I think the new binding syntax is good is that it makes component inputs distinct from attributes. It makes it clear which attributes are just attributes and which are actually component inputs. NOTE: you can actually supply a component input without the bracket syntax if it's a static value that won't change, but in most cases we will use the brackets syntax - so this point I'm making still applies in general. In AngularJS directives, directive inputs (specified via the scope property in a directive definition object) look exactly the same as plain old attributes - you have to look inside the directive definition to see which attributes the directive will actually be using, or make sure you use a consistent prefix for your directive inputs.
Another cool thing about Angular 2 binding is that it checks if we are binding to a known property (known properties encompass native properties and explicitly declared component/directive inputs). If we try to bind to an unknown property it will throw an error.
The NgModule feature only appeared in RC4 - this was not a trivial change for us to incorporate into our application, which already included dozens of components we had written. Overall I feel this was a good addition to the framework. It gives us a way to group related functionality under one module, helping our app to be better organised. It's also important for having the ability to lazy load modules in our application. In our application we have certain editing functionality only available for verified users. In the near future we will probably ammend our routing to guard edit routes, and lazy load our EditModule only if a user turns out to be verified. As many of our users are anonymous (no user account registered, or not signed in), this will mean we won't make them waste unnecessary bandwidth downloading functionality they can't use.
I won't compare to other popular competing frameworks, but compared to our previous AngularJS application the performance is really good. A more optimised change detection strategy is the main reason behind this, compared to AngularJS dirty checking, which gets out of hand on functionally heavy pages. Angular also provides AoT (Ahead of Time) compilation which should improve rendering speed even more - we are currently using the default JiT (Just in Time) compilation which is performed in the browser, so still more performance gains are possible.
Later on we may use Angular Universal on the server to do the initial page render and send back the HTML instantly. This means something gets displayed while the frontend application bootstraps, which will improve application load times for the user substantially.
Angular comes with an interesting library called zone.js, also maintained by the Angular team. It modifies global functions
relating to asynchronous actions (e.g. setTimeout, setInterval, Promise
) so that Angular knows when data may have changed.
You can see this by typing Promise.name
in the console of an Angular app and you'll get back "ZoneAwarePromise"
instead of "Promise"
. Remember in AngularJS having to do $scope.$apply()
? This is gone with the new Angular
framework - now Angular knows when our data could have changed and runs change detection after asynchronous code has executed.
This is pretty awesome, even though in general it's a BAD idea to be modifying native JavaScript functionality.
Angular allows us quite good control over the change detection tree in our application. For some particular component, we can specify that we only want it to be checked if the component inputs change (object reference check - no deep checking), which allows us to optimise our application. We can manually trigger change detection and can even detach and reattach change detectors. This is a huge improvement since AngularJS - we had very little control over its internal dirty checking mechanisms.
Building this application is my first time being exposed to the Redux data flow. When I started building my prototype I didn't totally understand it, but eventually it clicked for me and made total sense. I think it's really good to use in a complex application as it means our components don't need to know how to change application state - they just render the view based on the data they recieve, and when data needs updating they just dispatch actions - reducers will take care of the application state manipulation. This results in a better separation of concerns and makes our components easier to test - our component tests only need to ensure that the components render their recieved data correctly and dispatch the appropriate actions when expected. Testing the data manipulation is instead done in reducer unit tests.
The package @ngrx/store is a good choice for angular applications compared with the original Redux package because it works directly with RxJS observables, which are already a dependency of the Angular framework itself.
As we are using the Redux data flow, our data in the store must not be mutated. We chose to use ImmutableJS to help with this. It has been quite a complex process, because some of our data models are quite complex objects with nested sub-objects. Despite extending development time, it is an important part of our application because we need to make sure nobody is inadvertantly mutating application state. If such things happen unintentionally without our knowledge, it could cause unexpected side effects in our application, which could be very hard to determine the cause of.
At first, this was a daunting topic for me. I was completely new to Observables. Angular uses RxJS for its Observable implementation, which is a huge library by itself. It takes some time and persistence to get a good grasp of it, but in my opinion it's quite a cool pattern. It definitely has some benefits over Promises - the main one being emission/subscription of multiple values asynchronously instead of just one. We have adopted the observable pattern and are using observables extensively in our own services.
You do need a good understanding of RxJS especially when using more complex operators. One trap we fell into was using the
share()
operator without truely understanding what it does. This resulted in some situations where we
had multiple subscribers to an observable but only one of them was notified when a new value was emitted. It's worth
taking the time to get your head around RxJS before jumping straight into using it in your Angular app!
Prior to starting our Angular 2 project I had very litle exposure to testing. We make sure we write tests for all our reducers and services as these are the most crucial parts of our application - we need to make sure all the pieces related to data flow and application state management are working correctly. We also have some component tests for our Angular components, but not all of them at this stage.
We are using the Karma test runner and Mocha for our testing framework, along with Chai.js for expectations and
assertions, and sinon.js for mocks, stubs and spies. Though time consuming, I would rate the testing experience
with Angular 2 to be fairly good. One thing in particular I find quite useful in some tricky testing situations
involving asynchronous execution are the test helper methods fakeAsync()
and tick()
provided by the Angular testing package. These can be used to run async code in a synchronous manner. You supply your
test function to fakeAsync()
and you can call tick()
anywhere inside your testing function
to process any pending async tasks synchronously, such as observable emissions.
If you made it this far - thanks for reading through all the way! This summary of my experiences turned out a bit longer than I thought it would. I'm not very used to writing about my development experiences yet so hopefully it wasn't too incoherent...
Overall, I personally have no regrets over our decision to build this new application with Angular 2. It's far more performant than it's predecessor AngularJS, and I find the whole development experience to be pretty enjoyable. I'm trying to learn React and Vue in my spare time so maybe one day I'll be able to draw more direct comparisons between the frameworks but until then I'll continue on content with Angular 2 :)
Have you got any experiences or opinions on Angular? Plesae share them in the comments below!