Disclaimer: This post will not be looking at what is Angular Ivy, instead it explores some of the more experimental features that Ivy makes possible. As these features are experimental, they may be subject to change. If you are looking for a more in depth post on the ins and outs of Angular Ivy, check out Eliran Eliassy’s post on the new Angular Engine or the presentations from this year’s AngularConnect conference.
This leaves the company with a situation where they are supporting multiple frameworks, and often need the same feature or component in multiple projects. Short of burning everything to the ground and starting again with a more unified approach, could there be a smarter way of avoiding duplicate work? This article explores some of the more experimental features made available with Angular Ivy that has features which can be used to create cross framework shared component libraries.
One of the trending solutions to the cross framework problem is to use web-components. Once compiled, these web-components may be standalone elements that used within any framework. Whilst this article is not discouraging their usage, it is worth noting they do have their own drawbacks. For example, one issue I’ve encountered is how to include web-components in Angular libraries efficiently. This could pose issues when a user imports a library importing the web-components and also includes the web-components in the App. Whilst not an insurmountable challenge, it shows web-components aren’t always the best tool for the job.
Additionally, if you already have a component library written in either React or Angular, you probably don’t want to first convert these to web-components before you can use them. This leads to the question, can a React component library be used directly in Angular or vice-versa?
Can you use React components directly in Angular or Angular components in React?
Trying to figure out if it is possible to use React components within Angular, I came across an ng-packagr document and demo app that does exactly this. In this sample, a React component is rendered directly inside an Angular component using the
ReactDOM.render function. This integration component could then be bundled as part of an angular library and used directly within an Angular application.
The simplicity of this solution of just using the framework’s native render function to inject the component led me to ask if the same could be done to inject Angular components into a React application. Prior to Angular Ivy, this would have been a more complex task. However, with the introduction of the new Ivy renderer, it has become easier. It is now possible to render standalone Angular components with minimal work as demonstrated by Eliran Eliassy’s “Bye Bye NgModules” talk at NG-DE and corresponding article.
The biggest change that Ivy has introduced is that a component now knows everything it needs to render itself. Prior to this change, a component could only be rendered within an Angular context, as it required the rest of angular to be able to render it. Rendering a component in Ivy is as simple as
From the Angular renderComponent function documentation, the function:
Bootstraps a Component into an existing host element and returns an instance of the component. Use this function to bootstrap a component into the DOM tree. Each invocation of this function will create a separate tree of components, injectors and change detection cycles and lifetimes.
For our challenge of rendering an Angular component within a React context, this function sounds like it does exactly what we need.
Rendering an Angular Components Library in React
Now that we know what angular tools will be useful, let us take a look at what we will need to create a demo app. First we will put together a couple of simple Angular components and compile them with Ivy as an Angular library.
Next we will create a React wrapper library. This library will exist as one “wrapper” React component for each Angular component in the library. This will enable us to make the library tree-shaekeable and filter out the components we don’t end up using. To avoid writing duplicate code, as these wrapper components will be almost identical to each other, we will create a generic wrapper component class that we can then extend into each of the wrappers.
Creating the Angular Library
The first component we will create is a simple Hello World component. This component will have an input string which it prints in the template and an output event that emits after one second. In order to tell the component when to emit the event, we will use a
setTimeout in the
ngOnInit lifecycle hook. Finally, we will listen to the
ngOnChanges lifecycle hook, and log the change object to the console to demonstrate that everything is working as expected.
This component demonstrates a few things. First, it demonstrates that basic input and output binding will still work when we put this in a React environment. Next it will demonstrate that we can still use angular lifecycle hooks and change detection can still run as expected in default mode.
The second component we will create is a Timer component which again uses Angular lifecycle hooks, but also uses rxjs observables with on-push change detection to update the counter. For on-push change detection to work, we need to tell the component when the view is dirty and needs updated. For this we will use the ivy
θmarkDirty function. This function informs the Angular runtime that the component has changed and should be checked on the next change detection cycle.
Creating a React helper
Next, we will create a generic React component helper class. This class will tell React how we should render the Angular component it is given. The point of it is to reduce the amount of code required to create the more specific “wrapper” component. This generic class will use several of the new Angular Ivy functions to render our angular component to the DOM and keep it updated as events happen.
Using typescript language features, we can define this re-useable base as accepting a type
T , which represents the Angular component we are to render. This way we can define a component that can render any Angular component rather than being too specific. This generic component will also extend the basic React Component, allowing all components inheriting this class to be understood by React.
The constructor function for the class will take a props object and a
ComponentType object of the Angular component we wish to render. The
componentType is a special type interface used by the Angular Ivy compiler which extends the bas
Type and adds a special property, the key of which is the value of the
NG_COMPONENT_DEF variable. This property provides additional meta data about the component we wish to render such as the selector, the inputs and the outputs.
By accessing this special property of the component, we can simplify what information the later wrapper components need to provide about the Angular component. Additionally, by using the Angular specified host element, we make the final code more verbose and easier to read as it will be included in the html.
Finally, in the constructor, we are storing the selector in the component state object with the
selector key so that it is easily accessible later during rendering of the component template.
Taking a quick look at React lifecycle hooks and their ordering we can see that the
render method runs in the mounting phase before anything has been added to the DOM. This
render method is required in all React components and declares the template and how it should be rendered to the DOM. Until the first render has run, there is no DOM template onto which we can hook our Angular components.
Therefore, we will make our
render method, it will create a simple host element for our Angular element to hook onto, but we will not perform any actual Angular magic here. Our
render method will grab the selector we stored earlier and create a DOM element with the element tag matching the selector defined in the Angular component. This provides a places for the angular component to insert itself into the page DOM.
As there is no rendered template until after the first
render has run, we will hook into the
componentDidMount React lifecycle hook. This hook runs once after the initial render, at which point we will have a host element in the DOM that we can hook into. The
componentDidMount React lifecycle hook can be thought of as being roughly equivalent to the
ngAfterViewInit hook in Angular.
componentDidMount hook, we will call Angular Ivy’s
θrenderComponent function. This function will take the Angular component (supplied in the constructor) and tell Angular to use lifecycle hooks. Without explicitly telling Angular to use the lifecycle hooks, they won’t work.
Handling output binding between React and Angular components is a little more challenging due the differences in how event binding is implemented. In Angular, output events are handled with event emitters, which are extensions of rxjs
Subject classes. This means that in Angular when you are listening to a component output, what you are actually doing is subscribing to an observable and executing some code when that observable emits.
React takes a different approach to output events. In React, a component will pass a function as a prop input to a child component. It is then the job of the child component to call the passed in-function.
To get around this difference in approach, our React wrapper components must subscribe to the individual output event emitters of the Angular component. Within this subscription function, we can call the corresponding React prop function. As we have access to the Angular component definition, we can look at the defined outputs and loop through each one and subscribe to it if there is a corresponding React prop and setup the chain to pass on the event. Lastly, we store all these subscriptions in a private class property so that we can unsubscribe to them later to avoid memory leaks.
The final section of the
componentDidMount function is to update the input binding. Here we are wrapping this into a
updateComponent function as we will need to check if the inputs need updated on every change detection cycle.
updateComponent function loops through each react prop and sets the value of the matching angular component property. Setting the property values directly rather than via the template does not inform Angular that a change has occurred. Therefore, we need to manually call
ngOnChanges and Angular Ivy’s
markDirty to inform Angular about what has happened.
Next we will look at how we update the Angular component when the React component runs an update cycle. As we saw in the lifecycle hooks diagram above, there are 2 hooks that get called during the update cycle:
render, which we are using to add the host element to the DOM and
componentDidUpdate. We will use the
componentDidUpdate function to update the input binding on every update cycle of the React component. As we already have a updateComponent function created to update the inputs, all the lifecycle hook needs to do is call it.
Finally, we will use the componentWillUnmount hook to cleanup our subscriptions so that we don’t get any memory leaks.
With that we know have a generic React component that can render almost any Angular component it is given.
Creating React wrapper components for each of our Angular components
As we have created our generic react component, we can now just extend it to create our wrapper components specific to the different Angular components. Creating our Hello World and Timer components can now be done in just four lines each.
We will then compile our React Wrapper library using the Angular compiler and ng-packagr. This is required for the final library to understand what to do with Angular.
The React Application
Finally, we will look at how we use the wrapper components within a React application and see what we have built.
Here we are creating a basic React component, which adds the Hello World and Timer components to the DOM. In the Hello World component we are using input binding to pass a name to the Angular component and logging an event to the console when the Angular component emits an event. Finally, we are changing the name property we pass to the Hello World component after two seconds.
The repository I used for the this article also includes the full working example, as well as a demo of going the other way and putting React components within Angular applications. Feel free to contribute ideas on how this could be expanded! See here for my GitHub Repository.