Much time has passed since the birth of AngularJS. In fact, AngularJS is already outdated, and given how fast front-end technologies grow, its value will depreciate further as new frameworks gain popularity and conquer the open-source community.
This is something we became intimately aware of recently at Tonkean. See, we have been using AngularJS for some time. We wanted to enjoy the advantages of coding in a modern framework, but we didn’t have the time to drop everything and rewrite the application. We are a startup and we need to move fast — we can’t maintain two separate codebases and deliver valuable features simultaneously.
We decided to switch to a new framework: React. But to do that, we needed a solution to help us use React and AngularJS coterminously, so that we could code new features in React and let AngularJS fade gradually.
We looked into existing libraries that enable combining React and AngularJS components in the same application, hoping to find an out-of-the-box solution.
Unfortunately, these libraries generally all failed to meet two major requirements:
- They didn’t use shared context when placing React components inside AngularJS components — all the existing solutions create a new root element for every component, instead of using a single root element.
- Callback and string binding was not supported when placing AngularJS components inside React components.
Through our research, however, we were inspired by how easy it is for a developer to export a component as React/Angular. So we decided to copy that pattern. Our goal was to create a framework where you can declare an export of a React component to AngularJS in just a single line:
From there, we turned to creating our own library that could embed new React components in our soon-to-be-depreciated AngularJS app, and vice-versa.
We wanted a super simple way for developers to create React and AngularJS components by traditional means and to then be able to include each in our app with ease.
Here we’re going to show you the basic concepts that led us to the complete solution. The solution is not open source yet, but if you’re interested in implementing such a solution yourself, we’d love to help you with more information.
Before We Start: About React Portals & Context API
React v16 introduced a new feature called portals. Portals are an API for rendering components outside of the DOM hierarchy. Some more traditional examples include dialogs, global message notifications, and tooltips.
In addition to using portals in the standard way, we used portals to inject a React component anywhere in our code while maintaining a single shared React context.
The React Context API provides a way to share values between components without explicitly passing a prop through every level of the tree (commonly referred to as “prop-drilling”). In complex production apps, it is used to pass almost all data into components (React-redux is basically a wrapper around context).
How it Works: React to Angular
In order to allow the rendering of React components inside AngularJS, we create AngularJS directives on-the-fly.
1. We wrap each component with a context containing an AngularJ injector, and create a custom hook named useAngularService that uses the context to get the injector and the service.
2. We use the $onInit lifecycle hook of the controller of an AngularJS component to create the React component (We have to get all the bindings from Angular and know how to re-render the component when they change). We also define a callback for removing the component from the list of components to render, using the removeComponentDefinition function.
3. We initialize an UpdateableComponent; it’s a React component that renders the needed component. We use the React hook useImperativeHandle, which allows us to create a custom React ref object. The object we create has an updateProps method that updates the props state and triggers a re-render.
4. Here is where the magic happens. We bind all components to a single React Root. We add the React component to the HTML of AngularJS’ template using React’s portals.
5. This is the way we generate the portals:
6. We wrap React’s portal with our own `Portal` component.
7. Watch for changes; we use the $onChanges lifecycle hook to generate and update the binding object and trigger the `updateProps` method, which will update the React component.
8. We subscribe to AngularJS’ destroy lifecycle function to unmount the React component.
Remember the example of the component we did at the beginning of this post?
This will look in the HTML as follows:
How it Works: Angular to React
Now we’ll show you the other way around. The AngularJS component is compiled once, and the React component is created once on the component’s initialization.
A digest is fired on every React props change (much like it would have been fired with a regular Angular component).
1. Next, we create a new scope for the component. We create the angular scope for the component, and when the component unmounts, we destroy the scope.
2. We update the scope with the updated props, compile the component if it’s not compiled yet, and queue a digest cycle. This `useEffect` runs on every rerender and therefore has no dependencies array.
3. We wrap the component with `React.memo` so we can be sure that it’s being re-rendered only when one of the props changes.
And in the JSX…
Now that our framework is alive and kicking, it has become possible to migrate our application gradually. However, there are always other obstacles that show up when migrating an application. It’s essential to keep in mind that all the solutions above are a middle ground between AngularjS and React. Our final goal is to get rid of all of them and use React’s conventions and best practices — and, of course, so we can continue to enjoy writing code 🙂
If you find any opportunities for improvements or better solutions, contact us at Tonkean, we’d love to hear from you.