Integrating React Components into an Angular 2+ Project

In my final few weeks at RateSetter, I was working on one last, organism level component to add to our RateSetter Component Library.

We had created multiple "vehicle lookup" UIs that were implemented differently across four different projects (two React and two Angular 2+). When we wanted to add features to this vehicle lookup UI, we found it difficult and time consuming to go through each project, understand how the vehicle lookup was implemented and then update it.

We decided to create one component that would be consumed by each of the UI projects. This allowed us to write code once in the component library, publish it and then simply update the component library dependency in each project. It resulted in more rapid, bug-free development.

I whipped up the vehicle lookup component using React (with TypeScript), Hooks and Context and added it to our component library. Integrating the component within React projects was trivial, but integrating it within Angular 2+ projects was an unknown for me.

After a bit of research and experimenting, I found that integrating a React Component within an Angular 2+ project is surprisingly simple! Follow along to find out how I did it.

Note: This article uses react + react-dom @ 16.13.1  and Angular @ 9.1.0. I used @angular/cli to spin up a basic Angular project called react-integration-app.

Integration!

React Component

Let's say you wanted to integrate the following React component into an Angular 2+ project:

import React, { useCallback, useState } from 'react';

export interface IFeelingFormProps {
  name: string;
  onSubmit: (feelingUpdate: string) => void;
}

const FeelingForm: React.FC<IFeelingFormProps> = ({ name, onSubmit }) => {
  const [currentFeeling, setCurrentFeeling] = useState('');

  const onFeelingChange = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setCurrentFeeling(event.currentTarget.value);
    },
    []
  );

  const onSubmitEvent = useCallback(() => {
    onSubmit(`${name} is feeling: ${currentFeeling}`);
  }, [name, currentFeeling]);

  return (
    <form onSubmit={onSubmitEvent}>
      <label htmlFor="feeling-input">How are you feeling?</label>
      <input
        id="feeling-input"
        onChange={onFeelingChange}
        value={currentFeeling}
      />
      <button type="submit">Send feeling</button>
    </form>
  );
};

export default FeelingForm;

Dependencies

To get React components rendering in your Angular project, we need to install React. In react-integration-app run:

npm i --save react react-dom

It also helps to have React types installed:

npm i -D @types/react @types/react-dom

Also, don't forget to install your component library!

Angular Wrapper

In your Angular project, create a new directory in /src called react-feeling-form and in it create react-feeling-form.component.ts. In it, add:

import {
  Component,
  OnChanges,
  Input,
  Output,
  EventEmitter,
  AfterViewInit
} from '@angular/core';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { FeelingForm } from '@harvey/harvey-component-library';
import { IFeelingFormProps } from '@harvey/harvey-component-library/build/feeling-form/feeling-form';

@Component({
  selector: 'app-react-feeling-form',
  template: '<div [id]="rootId"></div>'
})
export class ReactFeelingFormComponent implements OnChanges, AfterViewInit {
  @Input() name: string;
  @Output() submitEvent = new EventEmitter<string>();

  public rootId = 'feeling-form-root';
  private hasViewLoaded = false;

  public ngOnChanges() {
    this.renderComponent();
  }

  public ngAfterViewInit() {
    this.hasViewLoaded = true;
    this.renderComponent();
  }

  private renderComponent() {
    if (!this.hasViewLoaded) {
      return;
    }

    const props: IFeelingFormProps = {
      name,
      onSubmit: (res: string) => this.submitEvent.emit(res)
    };

    ReactDOM.render(
      React.createElement(FeelingForm, props),
      document.getElementById(this.rootId)
    );
  }
}

Before trying to understand what's being done here, have a good read through React without JSX.

We've created a template with a div element that will act as our container to mount our React component on. We're binding the id attribute to the rootId field in our Angular component to make it easier to find in the DOM.

We're accepting accepting props that match what our React component requires - @Input() for name and an @Output() Event Emitter for our onSubmit callback.

ngAfterViewInit is used to inform our component that our container (view) has loaded and is ready to have the component mounted on it. It also performs the first render.

If we don't check the view has loaded before attempting to mount our React component (which will happen because ngOnChanges runs before ngAfterViewInit),  we'll run into this error:

ngOnChanges is used to "re-render" our React component. Anytime one of the Angular inputs change, so should the props provided to our React component. From React's documentation:

If the React element was previously rendered into container, this will perform an update on it and only mutate the DOM as necessary to reflect the latest React element.

React.createElement is used to create our React component, passing through the props our Angular component received. This is identical to <FeelingForm {...props} /> in JSX.

ReactDOM.render takes our freshly created React component and mounts it to our container element (which is retrieved from the DOM by rootId).

Important: We will also need to add the following to tsconfig.json:

...
"skipLibCheck": true,
"allowSyntheticDefaultImports": true
...

skipLibCheck will prevent TypeScript compiler issues when that occur when React is present. allowSyntheticDefaultImports is needed to prevent errors due to how React is exported.

Also, don't forget to add the new component to your app.module.ts Declaration field.

Using the wrapper

You would then use the wrapper component like any other Angular component. For example:

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <div className="app-container">
      <h1>Below is the React Component!</h1>
      <app-react-feeling-form
        [name]="'Harvey'"
        (submitEvent)="submitEvent($event)"
      ></app-react-feeling-form>
    </div>
  `
})
export class AppComponent {
  submitEvent($event: string) {
    alert($event);
  }
}

After running ng server you should see:

And by clicking Send feeling you'll see the alert:

Component Library Styles

If your component library exports styles as a standalone CSS file, you'll need to add it to angular.json under the styles field, for example:

...
 "styles": ["src/styles.scss", "./node_modules/@harvey/harvey-component-library/build/styles.css""]
...