In my last blog, How Capital One is Using Angular Elements to Upgrade from AngularJS to Angular, I gave a deep dive into Capital One’s massive Angular transformation of our customer servicing platform. Today, I want to build on that by sharing with you the top lessons we have learned and embraced during our migration journey. Some of these lessons are human focused, and others are centered around technical challenges we have faced.
If you haven’t already, check out our Ng-Conf 2019 talk on how we are using Angular elements!
- Preparation — Educate your developers.
- Plan — Don’t put the cart before the horse.
- State management — You may or may not need it.
- Component libraries — Design systems can win half the battle almost immediately.
- This is your second chance at a better life — Migration is an opportunity, not a burden.
- Tooling is your friend — Pick the right tool for the job.
- Communication — Establish a committee of stakeholders for important, high-level decision making.
- Defining migration patterns — Define best practices for your team.
- Framework communication — Defining communication patterns from AngularJS to Angular.
- Change detection quirks — One does not update what it does not know about.
Alright, Let’s get into it!
Lesson #1: Educating your developers with the new framework’s underlying concepts and technologies (e.g. RxJS), is the best first step before any migration work begins.
Think back when AngularJS was in its prime. Your application used MVC architecture and
Promise-based logic to resolve asynchronous work. Now fast forward to present day Angular. Application architecture has shifted to a more modular, component-based approach coupled with composable data streams called
Observables via RxJS to handle asynchronous work instead. This is a major shift in the underlying architecture of the frontend frameworks we use to power our applications in only a handful of years.
When first learning Angular, it’s easy to overlook RxJS. It is difficult to master, but crucial to Angular, as it is the basic building blocks of the framework. That being said, reactive programming requires a major mind shift from Promises to Observables because of how it handles asynchronous events. Instead of one time fetch and receive interactions, RxJS reimagines these interactions as streams of data that can be extracted and manipulated as data is emitted through the stream. Many developers tend to make common mistakes in RxJS such as nesting subscriptions, subscribing in the wrong place, or failing to unsubscribe at all. RxJS is incredibly powerful, composable, and flexible. At the end of the day, mastering RxJS is what separates good Angular developers from great Angular developers. Since Angular leverages it so heavily under the hood, study up!
Lesson #2: Don’t put the cart before the horse — lay the plumbing down for core application resources that all your features will need before attempting to migrate feature code.
If migrating to Angular has been on the horizon for a while, it might be tempting to dive head first into the code, and go full throttle with migrating features. It’s absolutely possible to use AngularJS providers in Angular by grabbing a reference of that provider via the
$injector; however, resist the temptation! It can almost be guaranteed that core services like i18n content, authentication, data hydration, and whatnot, will need to be migrated and available in Angular first, since these features will most likely depend on them. If these services are not converted before the features themselves, bridging AngularJS providers can introduce a number of complexities that can become difficult to manage over time. Complexities can include (but are not limited to):
- Keeping track of services that have been migrated to Angular versus services that are still in AngularJS.
- Remembering which features originally consumed a bridged service, but then later migrated to a pure service — over time, it’s possible that implementation details might have changed.
- Having to prevent duplicate migration efforts of those shared resources.
- Retrofitting feature work with implementation changes from the service.
Given these complexities, having these critical services established beforehand will not only make the migration smoother, but it will reduce the cognitive load of what is “in-flight”, including the likelihood of the implementation changing over time before your feature migration work is complete.
3. State Management
Lesson #3: Decide if state management is necessary for your application up front. Chances are, if you haven’t needed it yet, you may not need it now.
State management can be a somewhat controversial topic, and it happens to be one of those things that Angular has yet to take an official stance on. There are a handful of great libraries out there that the Angular community stands behind, including NGRX and NGXS for redux-styled patterns, and Akita (which functions like a database with tables) for a more object-oriented approach.
Even though you can opt-in to a state management library at any point during the lifespan of an application, it is advantageous to do it up front while features are being migrated. There are a plethora of comparison articles out there already, so I won’t get into the nitty gritty details of which state management library you should pick. Instead, I encourage you to do your homework and see which is best for your use case.
If all else fails, many developers have opted to simply use
BehaviorSubjects in their Angular services to maintain the state/data for that service. Dan Wahlin gave a great talk at Ng-Conf 2019 about how developers can use RxJS to communicate between components without needing to opt-in to a state management approach.
4. Component Libraries
Lesson #4: Utilizing a design system can win half the battle almost immediately.
Using a component library can save developers hours, if not days, of having to recreate the wheel when it comes to common user interface widgets. Every web application uses date pickers, input boxes, drop down menus, radio buttons, and so on. Many design systems do the heavy lifting of ensuring that everything has a consistent look and feel while ensuring that they remain accessible for users. While it isn’t required to develop your Angular application with dedicated design systems, it removes a lot of the burden from the developer when it comes to design. Here are a few popular Angular component libraries:
If none of those suit your aesthetic needs, Angular offers Material Design’s baseline foundation called the Component Dev Kit (CDK) to developers. Think of this as an “un-skinned” version of Material Design that can be styled to your company’s specific design language.
5. This Is Your Second Chance At A Better Life
Lesson #5: Migration is an opportunity, not a burden.
Try to rethink each feature from the ground up. Chances are, the code isn’t as clean as you’d like, or it could be done in a better, simpler way with the newer technologies available today. Even if you don’t feel the need to re-engineer a specific piece of code, a little cleanup is better than nothing. Live by Uncle Bob’s boy scout rule —always leave code cleaner than you found it.
If I can emphasize one key lesson, it’s that migrating features from one framework to another will most likely require the features to be architected differently than the legacy framework — it’s not one-to-one. Don’t try to fit the existing implementation into the framework. Instead, leverage the new methodologies the framework provides for a cleaner, more performant solution.
6. Tooling Is Your Friend
Lesson #6: Pick the right tool for the job.
There are many different tools out there that do very specific things for our applications. We have builders, minifiers, obfuscators, static code analyzers, CLIs, test runners, formatters, linters, schematics, and so on! The tools that you choose, and how you use them, can greatly impact the developer experience, so pick carefully.
Here are some popular tools that we have found to be useful, that play very nicely with Angular:
During our journey, we’ve encountered a real need for efficiently building our application with Angular elements. Each element that is created requires an independent build process in order to package the element so that it is made available to the application. This build process can be incredibly CPU intensive when we have to build dozens of these mini Angular applications. Thankfully, our friends from NRWL have made the developer’s life much better with their expertise in these complex situations, and we couldn’t be more grateful for their services.
Lesson #7: Establish a committee of stakeholders for important, high-level decision making.
As the floodgates open, and developers are deep into migration efforts, a variety of issues may come up that can impact the entire application as a whole. This could be anything from defining patterns, picking specific third-party libraries, or picking a state management library. Regardless of the issue, establishing a team that can provide guidance, and decision making for the application, can have a huge impact. This can help focus the efforts of the developer by removing the burden of having to determine these design decisions on their own, preventing various implementations that do the same thing, and optimize for the best approach.
In our experience, we have many teams who contribute to our platform, so we have elected to have one representative from each major group of developers to take part in a committee of stakeholders. Doing this introduces a democratic dynamic that allows each group of developers to express their needs, concerns, or even propose ideas to make the overall developer experience better. Additionally, any decisions made by this committee can be communicated to the larger developer group from that representative. We have seen great success with this model, and I would highly recommend this approach.
Additionally, this same model of a stakeholder round table can be applied to smaller subsets of developer groups. This allows these developer groups to choose a handful of developers to raise any concerns, missing best practices, or new patterns that should be followed by other developers outside of the group.
8. Defining Migration Patterns
Let’s highlight some of the more interesting technical problems we’ve encountered because of the way we are using Angular elements as a tool to upgrade.
Lesson #8: Define coding best practices for your team.
This is a great opportunity for you to establish, or change, the norm of code standards, application and feature structure, best practices for both TypeScript and Angular, feature design, clean code definitions, and much more. Many teams have their own style of going through the development process, and this would be a great time to recalibrate the way things are done.
In my experience, my team has found great success in dedicating feature design sessions where we take an existing feature and whiteboard how we would like to see it designed in Angular. Since there are fundamental framework differences, we can design it from the ground up while migrating the business logic from the legacy code base.
Additionally, since the nature of migration work is different from standard feature development work, it’s important to remember that there will be a learning curve and adjustment period. Sometimes development time may take longer than anticipated, and that’s okay. Understanding where the estimations were off and what blockers may have come up during the migration work in order to improve the overall migration process.
9. Framework Communication
Lesson #9: Communicating from AngularJS to Angular.
Throughout our migration journey, we have heavily utilized AngularJS providers (e.g.
constant, etc.) directly in our Angular code without having to migrate the legacy code first. Yep! You read that right. In my previous blog post, I mentioned that we are bootstrapping both frameworks when the whole application loads. After each framework has bootstrapped, we are able to grab references of AngularJS providers and use them in our Angular code directly. Anything that is declared and registered on the
$injector of AngularJS can be used in Angular.
In order to use that AngularJS provider, we can actually access it through the window object within Angular. From there we can drill down to the
$injector where it has its regular AngularJS APIs! This is great because that means we can grab an AngularJS resource and use it anywhere in our Angular code. In our experience, we have found great success creating “wrapper” services that grab references to the AngularJS providers and make them available via Angular’s dependency injection system. This allows us to create one class that will maintain the consumption of this legacy provider in one service, and when we are ready to migrate the provider’s logic, we only need to update the code in one place.
From here, we can dependency inject the provider into a smart component and use the AngularJS resource exposed by our service to grab our data (or invoke other legacy methods) and migrate our feature without moving any legacy provider logic. Let’s take a look at what that could look like:
10. Change Detection Quirks
Lesson #10: One does not update what it does not know about.
Throughout our migration efforts we have run into a few odd incidents where the DOM was not rendering any data updates and the change detection didn’t seem to be working as expected within Angular. No matter how we implemented a feature or designed a service, automatic change detection didn’t seem to pick up any events that were occurring when we were trying to utilize AngularJS resources bridged by the wrapper service. This is by no means any cause for alarm though! It actually makes sense when you think about the circumstances.
With both frameworks being bootstrapped separately and not knowing that each other exists, the events that are produced in either framework have zero impact on the other framework. That means that events in AngularJS won’t cause Angular’s change detection to kick off when the event resolves, and vice versa. For example, if we bridge a method from an AngularJS provider that returns a
Promise, when that
Promise resolves/rejects, that event is handled within AngularJS, and that event is never picked up by Angular.
That being said, we established a few approaches to ensure that change detection occurs when necessary:
NgZone— This has been the best approach that we have found when it comes to hooking back into the change detection system.
run()method that allows us to opt back into the automated change detection system, and this has proven to be the most reliable approach. Additionally, we have created a utility function that allows us to convert a
Observablewhile hooking back into
NgZoneall at the same time. This has been incredibly fruitful since a majority of the asynchronous work being done in AngularJS is
Promisebased. The key thing to note about this is that hooking into
NgZone.run()is best used inside services and not in components. By elevating the use of
NgZone.run()in the services of bridged resources, this removes the overhead of having to implement
NgZonein every component that consumes the service that invokes an event that resolves in AngularJS.
changeDetection.OnPush()— This option is the last resort for getting change detection to work for you at a component level. If there is a limiting circumstance that prevents you from using
NgZone.run()in your service’s data producer, this is your next best choice.
Overall, this migration journey has been incredibly challenging, but also very rewarding. At a high level, we have learned that preparation is crucial, planning is essential, communication can make or break it, proper tooling makes our lives easier, and defining scalable patterns that result in a high quality codebase. It has challenged myself and my other fellow developers to push the edge and pioneer this migration path to the next level. Through our efforts, we have built out a scalable, robust approach to using Angular elements as a vehicle to migrate to the latest and greatest of Angular.
Keep tuned for upcoming articles on some more technical topics around a deeper dive into communicating between the frameworks and how we are using NGRX/Store in AngularJS as a single source of truth between both frameworks!
DISCLOSURE STATEMENT: © 2019 Capital One. Opinions are those of the individual author. Unless noted otherwise in this post, Capital One is not affiliated with, nor endorsed by, any of the companies mentioned. All trademarks and other intellectual property used or displayed are property of their respective owners.