I wanted to take a moment and write down some of the things I have learned working with Angular for the past four years. These are some practices that I have found very helpful when building large applications. My suggestions and recommendations are based on my experience of trying to make a scalable platform that allows multiple developers to contribute to the codebase without stepping all over each other or introducing lots of regressions. This applies only to the latest version(s) of Angular.
1. Update package.json dependencies often
A while back, a company I was working for based their entire UI on a the Dojo library. This was a very viable choice at the time, but we were not diligent about keep that library up-to-date with the latest revisions, even though it was only being updated every month or two at the time. This was partially due to the extra workload, but also because of the risk of adding security flaws and bugs and not having time to test for both.
It was not really a problem until a couple of years later when we realized we were not just a version or two behind, but that the API of the library had changed so significantly that comprehending and understand the refactoring work involved became an almost insurmountable task. As a result, the codebase became stuck in a sort of hybrid state with some parts being updated while other remaining off-limits due to it being locked down with old technology. It was eventually left to wither in the background as newer, more flexible supporting applications grew up around it.
At the time, I understood this reluctance to update the versions of that library. But now, the world has changed with the advent of the NPM ecosystem. I had seen first-hand how letting technical debt pile up left us with a hefty bill, and thus, I have adopted a new strategy: I firmly believe you should check for updates in your package.json at least once a week at the minimum and do your best to keep it current. This comes with a few caveats and hiccups, but I assure you it is much better than the alternative.
My current approach is a follows: When it comes to dependencies in my package.json file, I keep them current with an almost fanatic attitude. I use the NPM check update library which provides an ‘npm‘ executable that queries for updates to all of the packages in the project. Once I see which libraries have been updated, I examine the results. Since the versions are given in Semantic versioning syntax, changes to minor and patch versions are updated immediately and then smoke tested. Major versions will usually prompt me to check out that project’s repository README to see what breaking changes have been made. In most cases, I will also implement and smoke-test those updates and see if there are any obvious problems. Obviously, the more extensive your testing facilities are, the safer this will all become. However, I have found that 90%+ of the time, there is little to no impact to the code base. The issues that do immediately become apparent can be handled directly, without having to spend countless hours trying to figure out which update just broke something (because by doing this so frequently, you will only have 2-5 updates a week to worry about). If I update, for example, my Immer package and the application immediately throws an error on startup, I know exactly what caused it and exactly where to look. Try doing that 6 months later after you find 40 different version updates to your package.json file and then try to find the time to update and test them one by one. Frequent checks will break this down into a manageable, bit-sized task that can be simple to resolve. Plus, you can file bug reports quickly and find out if others have hit the same issue since the conversation around that particular update with still be current. If you find a problem and 6 months have passed, the developer may be unmotivated to retro-actively apply some fix that you are requesting.
We have been keeping our package.json current on a weekly basis for nearly 4 years now and the number of problems caused by this approach has been surprisingly low. Every so often we will find a glitch that was caused by a package update that was missed by the smoke and unit tests, but it is usually easy to track down and helps us build up our suite of tests to prevent it from being missed again in the future.
2. Wrap external package implementations to make swapping easier
Related to the recommendation above, I was updating a logger library we used in a project. It turns out that the library had a major version point release and was changing its module structure, which meant it would no longer work with IE11 (big surprise). As much I would love to join this movement and leave all things IE behind, project managers never want to leave it behind and demand that we support that decrepit old browser. I began to contemplate changing out the logging package in favor of another. However, I realized that I had imported that particular library by name in the imports in at least a 100+ files in my application. This was not the end of the world, but with something as familiar and consistent as the use of a logger, I figured the interface between my code and any particular logging package should be easy to abstract behind a standard wrapper. I created a Logger class of my own, gave it the common logging methods such as debug(), info(), error(), etc and then used that to wrap the implementation of the actual Logger provided by this NPM library so it now resides in one single location. After about an hour of updating those 100+ files to point to my internal Logger wrapper, I now have a codebase that will allow me to swap out logger package implementations with only one touchpoint. This may not be feasible for every package you reply upon, but for things like loggers, toasters, export utilities etc, it may be worth it to try and isolate those libraries behind a common wrapper class that will not change. This will make it less painful to swap out NPM projects that provide the same function.
3. Make use of a redux style store (either NGRX or NGXS)
I cannot overstate this one enough: from the start you will want to have a reactive-style store as a “single source of truth” that you can use to display data in your components. While I have used NGRX for a couple of years now, I have a special place in my heart for NGXS. It is definitely more “angular-like” in its application and requires a lot less code to get the same results as NGRX. The only reason I cling to NGRX is the greater support base and its higher popularity. However, if I was putting together a small-to-medium sized application today, it would almost certainly use NGXS.
4. Intellij Plugins for Angular are helpful
We have chosen to go with separate files structure for the code, template, and stylesheet (ts/html/css) for each component. I find that our classes and templates are usually large enough that keeping them separate reduces the clutter. The majority of our developers use Intellij to edit their code. There is a great little plugin that will collapse those three files down into a single group in the project tree. This means you no longer see all three files representing a component, but instead see a rolled up node representing them all, which can be expanded to get access to a specific one. Check it out.
I hope you find these recommendations useful. If you have any questions or feedback, feel free to leave them in the comments section 🙂