The road to a hybrid approach
After hitting code execution performance and quality per invested development time bottlenecks with Angular (< version 2) one too many times, we decided it was time to come up with an exit strategy for Angular.
Completely building a new application from scratch was not an option.
We wanted to find an approach that allowed us to build new features in a more maintainable and performant way right now, while also laying the foundation for future upgrades of existing pieces of user interface.
- Minimize framework/library performance overhead for newly built features
- Lower the barrier to entry for contributions from any developer, no matter the background
- Gain access to the latest innovation the JS community has to offer
- No full rewrite at once, a gradual upgrade approach
The choice for React
We settled for React because of its performance and simplicity. Building web interfaces with simple concepts like reusable components and robust state management ticked a lot of our boxes.
With prior React experience already in-house, the choice was a no-brainer.
To enable a gradual upgrade process, we needed to find a way to use React components inside of our Angular code.
Bundling our Angular code with Webpack and Babel
Our Angular app was not yet built with modern JS bundling practices. We had to make sure we could pull this off first, or we would have no chance of cleanly integrating React.
The process of setting up Webpack to bundle our code opened many skeleton-filled closets. It forced us to solve some long-standing code maintenance issues and also gave us the opportunity to add some extra niceties:
- Injecting configuration (like API URLs) at build time with environment variables
.envfile support for easy development
- Reliable polyfilling
- Ability to use the latest syntax additions to the EcmaScript standard
- Auto-reloading development server
We might get into the weeds of this process in a spin-off blogpost.
Experimenting with Angular ⇄ React interop
The meat of this blogpost! 🍖
We deliberately did not want the possibility of rendering Angular UI inside React components as this introduces more dependencies on code we want to slowly phase out.
We had the luck of being on Angular version 1.6. It allowed us to use the
.component function for Angular modules to define Angular components, available since Angular verison 1.5. They are somewhat similar to React components on the level of state encapsulation, so the potential for a clean fit was already there.
We weren’t sure what the actual API was going to look like, but we knew we wanted something to generate an Angular component from a React component. The more it could autowire, the better.
To “reactively” render a React component inside an Angular component we need to be able to:
- Automount a React component inside an Angular component
- Pass Angular scope as React props
- Trigger React component re-render when Angular component scope updates
- Pass Angular callbacks as React props
- Trigger an Angular “re-render” when React calls a callback
Automount a React component inside an Angular component
Automounting a React component inside an Angular component is easy.
All we need is access to the DOM node of the Angular component.
To get a reference to it, we used Angular’s built-in
Inside the component’s controller function, we define a
$scope.$onInit handler function that simply uses the
render function of the
react-dom package to mount a React component inside the DOM node on component initialization.
Pass Angular scope as React props
Angular components are tied to a specific HTML tag name. We want to be able to pass scope variables to its inner React component as props like this:
Angular allows us to define bindings for components to achieve just that. There are a few different types of bindings provided by Angular, a full explanation is best left to Angular’s documentation.
We decided to use one-way bindings (signified by a
'<' in Angular) for regular variables, as these have the least potential for leading to performance degrading inner workings of Angular in extreme cases.
Inside the Angular component’s controller, we massage its
$scope (filled with the variables we created bindings for) into a
props object to pass into the React component.
To reduce boilerplate, we added the automatic inferring of a bindings definition based on the React component
.propTypes. We ran into a problem with this later on when hooking up callbacks, but more on that later…
Trigger React component re-render when Angular component scope updates
Angular calls the
$scope.onChanges handler of a component whenever the scope is updated.
render function again with a DOM node that already has a React component mounted results in a re-render instead, we could simply use the same function we defined for the
$scope.$onInit handler to cover our re-rendering needs.
Pass Angular callbacks as React props
In the beginning, we used
'&' bindings for functions. This was a two-fold mistake as you’ll see in a minute.
We generated bindings for our wrapper Angular component based on the React components
For props with a
PropTypes.func validation function, we added a
'&' binding, for all other
prop-types validation functions a simple one-way
We thought this implementation provided the most Angular-like developer experience on the Angular side, and the most React-like developer experience on the React side.
We were partially correct, but it also forced us to deal with Angular’s obtuse conversion of in-template functions to wrapped functions which expect arguments as an object instead of regular arguments.
This is simply what the
'&' binding does internally. It required us to either call Angular callbacks with an object instead of regular arguments inside our React code (bye-bye portable React components!), or write a wrapper React component to handle this mapping for us. By using
'&' bindings, we had to choose between weird Angular concepts infecting our React code or writing more boilerplate. This was the first negative consequence of using
When building our app with production-like settings, we discovered the different
prop-types validation functions got replaced with the same empty dummy function. We could no longer infer our binding types by comparing the
PropTypes.* function defintions. Our
.propTypes inferring mechanism just broke when not building/running the app in development mode. A big no-go, with no clean workaround. This was the second effect of our
The solution was actually very simple, but we needed to make our
'&' mistake first to realize what it was:
'<' bindings for everything! This way we can no longer pass in a function call with arguments into our component, only a bare function without any arguments. This reduces the amount of variable passing code in our templates, removes the need for handling Angular’s argument object mapping and allows us to keep using
.propTypes to infer bindings because we no longer need to check which
prop-types function is used to infer what binding to use! We now only need the object key names of the
.propTypes definition, which doesn’t get lost when building in production mode.
We probably could have gotten it working with both binding types eventually, but this approach reduced boilerplate and complexity. A great example of less is more in action.
Trigger an Angular “re-render” when React calls a callback
The final piece of the puzzle: making Angular re-render its template when we call a callback from React which could possibly affect
Wrapping all bound functions with another function that also calls
$rootScope.apply() while piping through all arguments inside the Angular component’s controller proved to be very effective and covered all of our “re-render on
When bundling up our approach into something that’s easy to use, we ended up with a simple function that would generate an Angular component definition based on a React component. No need to manually define bindings or other glue code.
We decided to name it
ngrc, “Angular React Component”.
This is all the code you need to register a React component for usage inside an Angular app:
import ngrc from 'ngrc' import MyReactComponent './MyReactComponent' angular.module('myApp') .component('myReactComponent', ngrc(MyReactComponent))
Reaping the benefits
We have already improved a bunch of existing user interface components, which you’ll be able to enjoy in the near future! More on this soon.
Implementation went fast and we have higher confidence in the quality of the end results for our users.
Can I use this approach for my own apps?
Sure! We made it available on NPM for anyone to use. Check out the package on NPM!