Today Angular 1.x is considered deprecated and many companies considering moving from Angular 1.x to a different framework like React, Vue or modern Angular. This, however, is not an easy task, especially if your Angular app is big like > 100k LOC. Since JS frameworks are relatively young there are no wide-spread and tested solutions that could help you figure out the right way to approach this problem. So generally you will try to invent your own solution and make some errors along the way of refactoring.
In this article I’ll describe some techniques you could use to make this migration painless and effective, based on real experience of migrating big Angular app to React.
Refactoring approaches: Rewrite vs Strangler pattern
There are two widely used refactoring approaches when it comes to general software systems refactoring: Rewrite and Strangler pattern.
If you’re using Rewrite, you will start with a brand new application and then try to mimic existing app features using new framework. This approach works great if you have not yet released your app and don’t have a burden of maintaining it for existing users. Here are some pros and cons:
- Clean architecture that fits new framework best
- High development speed, because you don’t need to build integration bridges between old code and new code
- If you want deliver new features to your app users, you’ll have to write them in old framework and then throw it away because you can’t ship new app until it fully mimics existing functionality.
However it’s painful to use this approach if you already have a big app in production. Therefore in working businesses you will find more often Strangler pattern used for refactoring.
approaches the refactoring problem differently. It assumes that you replace old components with new ones, taking care of stitches between frameworks. Thus you gradually remove old code and deliver new features using new framework.
- You implement new features using new framework
Maintenance burden for interaction of new and old code
You’ll have to align new framework architecture with the old framework architecture. And then probably refactor the new framework code as well
Both approaches has very serious cons, fortunately in case of Angular -> React migration, we can use best of two words to achieve optimal results. The idea is to use Strangler pattern, but pick big independent parts, Rewrite them one by one and then connect all this parts, thus finishing refactoring. The big question is of course how to pick these parts so that we can make the most effective refactoring. Below is the approach that works very good for Angular and React.
Angular router as a splitting point for refactoring
If you look at the top level architecture of the Angular app you will find that it’s basically an Angular router that delegates handling of routing events to Angular components. In medium sized app you’ll have about a several dozens of routes that use Angular components as handlers. Each page that the router handles is independent to a degree from other pages. So the strategy of rewriting each page that corresponds to Angular route works very good in this case. Here’s the gist of it:
Figure out your company’s plan for delivering new features (needless to say you have to align with your managers slower delivering pace for the new features in the period of migration. So only critical new features go here.)
Pick routes that are used for these features
Start brand new react components for each selected route, thus simultaneously refactoring Angular into React and delivering new features
That sounds easy, but let’s dive into technical details for that.
Bridging UI components: NgReact package
Arranging the Angular-React marriage would be tedious task, because they’re using somewhat different rendering engines under the hood. Thankfully, JS community already solved this problem. NgReact (https://github.com/ngReact/ngReact) package gives you the necessary glue between Angular directives and React component — it transforms all Angular attributes to React props and renders React component. Here’s the example usage:
Notice how it translates camelCase to html-case, i.e. HelloComponent in React becomes hello-component in Angular. The same thing goes for Angular attributes and React props.
With this lib it’s straightforward to start replacing Angular components used in Angular router with React components. Just pick some Angular route, bridge top level component to React and start implementing the whole React component tree below this component. You also need to keep data flowing from Angular router to React component, not the other way round.
Bridging business logic: Redux and Angular services
Sooner than later you will find dependencies between components from different routes via required data sharing. When one of these components is ported to React and the other is still Angular, you will still need a way to update React global state (Redux store in this example) from Angular component.
Here we have some Redux setup done and then on line 17 starts the interesting part. Basically it creates a factory named SessionActions (assuming we want to call Redux actions for Session from Angular component). This factory returns a hash with keys being action names and values being actions dispatched against Redux store. You can then inject this factory inside your Angular components and use it as you like. You might also want to add some bridge between Angular promises and React promises to make callbacks to these actions, though it’s not necessary strictly speaking as Redux actions are most often used in fire-and-forget way.
One last thing — is you better try to avoid this coupling at all if possible. You can try picking larger chunks for refactoring, so that bridge between Angular and React is minimized. That way you’ll have more clean structure and therefore won’t have hard times reasoning about the structure of your code.
Once the React components codebase gets large, you’ll definitely need to account for effects like route change. Unfortunately, at this stage you still have angular router, which means, you still need to pass some helper functions inside React to use Angular router API there. For example you’ll need functions like redirect, etc.
The easiest way to do this is to write a helper function that puts this function inside React context along with some other useful stuff like Redux store, etc. That way you’ll be able to handle routing in pretty much every component inside React. You have to be careful though, that despite being in the official documentation, React context was sort of “experimental” feature, but since 16.3 its new API is working according to this RFC https://github.com/reactjs/rfcs/blob/master/text/0002-new-version-of-context.md, so it’s more stable and usable now.
Finalizing — removing the router
So there’s just one step left before leaving Angular ground for the good — port the Angular router itself. This task is somewhat trivial, but painful, because router is the backbone for the whole app. So follow these steps to make sure you finish your porting successfully:
- Make sure you cover your code by e2e tests, or get your QA guys ready for reviewing the whole app
- Insert React router with the routes identical to those of Angular
- Hook up new redirect functions inside React context that use React router
- Test everything
- Delete Angular router and last pieces of Angular code
Now you don’t have any code left from Angular. But most likely you’re still left with some architectural legacy of Angular, because you were using Strangler pattern. So the final step is refactoring the app to a React-way and that’s it! You can now maintain and enhance your 100% React app.
Good luck with your refactoring journey!