How to refactor an Angular codebase

If you see a project ridden with problems and bad practices, it might be tempting to start refactoring right away. But it is important to clear up several issues before getting to work.

How to refactor an Angular codebase

You open up your computer, spin up a command line, and type ng new MyNewAwesomeProject, cd into the new project directory, run ng s and start a journey with a brand new Angular project, with the latest dependencies and stuff. Feels awesome, right? Well, sadly, this is not something we would have to deal much in our day to day life of Angular developers, as much as we would like it.  In reality, we would most likely have to deal with a project that has been created and maintained by other developers. And there are times that those legacy projects aren't developed in adherence to the best practices of the industry. It is very important to try our best to refactor a project like that to a degree that will make it easier to deal with it.

Before You Start

If you see a project ridden with problems and bad practices, it might be tempting to start refactoring right away. But it is important to clear up several issues before getting to work:

  • Make sure management and/or the client actually agrees on the need to refactor. Sometimes, especially on outsourced projects, the clients are just content with how the things are, and are unwilling to pay for more time spent on something the benefits of which they cannot visually comprehend. So it is important to clarify any such bold move with management.
  • Make sure your changes won't break the entire codebase. Understand the exact scope of what you want to change and can change, the stuff that you want to change but do not have the time to do, and the stuff that is so messed up that you don't want to touch it for the time being.
  • Create a separate git branch and work on, so you don't disrupt the usual workflow. Pull from your main development branch regularly to reduce the chances of painful code conflicts. We've gotta change lots of code!
  • Divide your process into clearly defined steps. This is what this article will mainly be about
  • Some steps will be way easier to implement and bring way more benefit in the form of performance, bundle size and code quality. Start with steps that are easier, and among them choose those that bring the most immediate improvements.

So, we have made sure our changes are welcome, and we have enough time to work on it. Let's get to work!

Add lazy loading

Lazy loading is a feature that is talked about a lot, but lots of projects out there don't have it implemented. If your project does not have it, it's relatively easy to implement, and will bring large improvements immediately.

Benefits:

  • Improvement in performance
  • Improvement in bundle size
  • Architecture may improve

Caveats:

  • Sometimes we would need to move some services up and down the provider tree. But in most cases this would be painless.

Update Angular to the latest version

Some projects that are not maintained properly might be using an older, or even completely outdated version of Angular. In this case, it is important to consider upgrading to a newer version.

Note: be careful when upgrading from version prior to 9 to 9 or higher - introducing Ivy may also introduce bugs and problematic behavior

Important things to consider:

  • I advise you upgrade version one by one, say, if you want to go from 7 to 10, go from 7 to 8, then from 8 to 9, then from 9 to ten. This way you might dodge some problems
  • Always use instructions from https://update.angular.io/
  • Do this before changing other parts of the codebase, because some changes may become obsolete after a major update.

Benefits:

  • Always better to have a current version of software
  • New features
  • Better performance and security
  • Possibly smaller bundle sizes

Caveats:

  • Some parts of your app might break and need manual fixing
  • Upgrade usually takes longer than anticipated
  • Some of your dependencies might not work well with updated versions of Angular

Fixing the folder structure

There are several different antipatterns when structuring Angular apps, and most of the applications fall for at least one of those. Now fixing the folder structure might feel like a tedious task, but it actually is almost risk free - as long as we are not significantly altering the code itself, we are pretty much safe, and when changing the folder structure, only the imports section of our code will be affected - and that is usually handled graciously by IDE-s. When changing the folder structure, be careful to follow these rules:

  • Group by feature rather than type
  • Move relevant services/pipes/directives next to each other
  • Store one entity (class, component, enum, interface, etc) in a single file
  • Follow rules from the official Angular styleguide

After doing this, consider renaming files and folders. Mention both the type and the feature of the file, for example "bookmarks.service.ts", rather than "bookmarks.ts" or "service.ts", even if it is stored under a folder called either services or bookmarks.

After this, files and folders in our apps will become significantly easier to find.

Benefits:

  • Better structured codebase
  • Easier to locate files and folders
  • Easier for new developers to get to know the project

Caveats:

  • Might take a while

Reshape the module structure

Let's say we fixed our folder structure (or even better - had a nice one from the get go), but our architecture is still messy. There is a number of things we can do

  • If there are many dependencies registered inside AppModule, consider moving them
  • Move most core dependencies to a dedicated CoreModule (like translation modules, initialization of core store features)
  • Move most commonly used entities across the app (directives or pipes used everywhere) to a dedicated SharedModule.
  • Other feature modules also might have their own SharedModule-s, so be careful to implement those carefully.
  • Use this part of the styleguide for better reference

Benefits:

  • Hugely improved application structure
  • Even easier to explain the project structure to other developers

Caveats:

  • Will definitely take lots of time to implement, so start with caution
  • Will probably result in some bugs, so prepare for rigorous testing

As mentioned, this is a step that will take a lot of time, so plan ahead carefully before starting; you don't want starting all over again when one design proves unsuccessful!

Start using the linter

Linting is a very important stage in our development cycle, as it allows us to remove common antipatterns and code smells early on.

Start by simply running

ng lint

first. It will give you the list of linting errors your application has. This will allow you to understand how much work will probably be needed to fix the apps. We can then run

ng lint --fix

to make the linter fix those linting errors it can. This will not fix all the errors, some will require a bit of manual work. Go on and fix the other errors, which should not be very hard at this stage.

Benefits:

  • Instant improvement to code quality

Caveats:

  • Sometimes you might disagree with the linter. This might be the time to read about linter rules.

Improve your Observables

So, we have moved the folders around, changed the module structure, and now our app has a better outward look. Let's get to changing the actual code of our app. And the best place to do that is to change the Observables we have.

Lots of projects suffer from abusing RxJS. Good news is, fixing problems with RxJS in most cases is very straightforward, and, if we are careful enough, almost risk free. Here are 3 essential steps:

  1. Start using operators. Take a look at your subscribe statements: chances are, you have lots of logic inside those callbacks that could be expressed better using RxJS operators. Use my first article on RxJS in Angular and RxJS best practices for guidance.
  2. Then take a look at where you use subscribe statements to 1) take the next value, 2) assign it to a property on our component and 3) display it in the template. Now try starting to remove those statements and switch to using the async pipe.
  3. Now start the most challenging part: start converting imperative logic where it is relatively simple to declarative logic using Observables. Take a look at my second article on RxJS in Angular for more information.

Benefits:

  • Relatively easily achieved, especially the first two steps
  • Huge improvement to code quality
  • As a bonus, sometimes may solve ExpressionChangedAfterItHasBeenChecked errors

Caveats:

  • Might cause bugs, as any code change
  • If you're unfamiliar with RxJS in depth, might require learning before starting

So what's next?

After we have done these mostly straightforward steps, our application must look way better than we found it, but it still can use some improvement. But those other steps will be more specific, so here are some of them:

  • Go through individual components and start improving the actual code inside them
  • Work on using more functional/reactive/declarative code vs imperative
  • Improve your directive/component selectors.
  • Use HTTP Interceptors
  • Consider using a state management library like NGRX.

We will discuss those steps in more detail in future.

And remember, there are always ways to improve our codebases even more!