Recently I’ve decided to switch to using React in a client’s SPA project currently developed in Angular 8. The way to go about such transitions is to build a bridge between the frameworks (i.e. React-Angular), use React in all new developments and gradually migrate old components to React, until the entire application is written in React.
I’ve just successfully completed such migration from Angular.js (Ionic-1) to React, but this time I needed a new bridge approach as we’re using Angular.
Of course there are frameworks that help using different technologies in the same application (e.g. Singla Spa) or Microsoft’s angular-react package however such solutions have their limitations and can be complex to implement properly.
Therefore I took a short journey and after some experimenting I believe I’ve come up with a simple and straightforward way to embed React components into Angular, which I will share in this post. Hope you find this method useful.
Solution Requirements
The basic required I laid out for the desired approach/bridge were:
- Props bindings / change detection must work
- Styling (css) must work
- React components must not be aware (i.e. hacked) of being embedded inside Angular
- Stateful react components (e.g. Class or Hooks) must be able to maintain state
- Must be incorporated into the same source project and use the same dev and build pipelines, in a transparent way and without any additional projects, scripting, tooling or hacks
- Has to be simple and easy to implement
Solution Implementation
The solution I’ve come up with consists of three parts:
React Support
In order for Typescript to recognize and compile our React code, we need to add the following property to the Angular project’s tsconfig.json:
"dependencies": {
...
"react": "^16.12.0",
"react-dom": "^16.12.0",
...
},
"devDependencies": {
...
"@types/react": "^16.9.17",
"@types/react-dom": "^16.9.4",
...
}
JSX/TSX Support
In order for Typescript to recognize and compile our React code, we need to add the following property to the Angular project’s tsconfig.json:
{
...
"jsx": "react",
...
}
Bridge / Wrapper Component
A small wrapper component needs to be created per each wrapped React component. The wrapper is responsible for detecting changes and re-rendering the wrapped React component so that its props take effect, eventually unmounting the wrapped component when the wrapper is destroyed.
The wrapper’s code can be seen below. Please note the following:
- Filename extension is .tsx
- The wrapped React component’s styles are imported by the wrapper via the @Component anotation’s styleUrls argument, so that it is bundled by Webpack
- Wrapper’s view was changed to ViewEncapsulation.None
- @Input and @Output members are created to mirror the wrapped React component’s props
- A placeholder element ref is created for mounting our React component into
- Lifecycle hooks ngOnChanges and ngAfterViewInit are implemented to render and re-render the React component
- Lifecycle hook ngOnDestroy unmounts the React component when the Angular wrapper component is destroyed.
////////////////////////////////////
// MyComponentWrapperComponent.tsx
////////////////////////////////////
import {
AfterViewInit,
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnDestroy,
Output,
SimpleChanges,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { MyReactComponent } from 'src/components/my-react-component/MyReactComponent';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
const containerElementName = 'myReactComponentContainer';
@Component({
selector: 'app-my-component',
template: `<span #${containerElementName}></span>`,
styleUrls: ['./MyReactComponent.scss'],
encapsulation: ViewEncapsulation.None,
})
export class MyComponentWrapperComponent implements OnChanges, OnDestroy, AfterViewInit {
@ViewChild(containerElementName, {static: false}) containerRef: ElementRef;
@Input() public counter = 10;
@Output() public componentClick = new EventEmitter<void>();
constructor() {
this.handleDivClicked = this.handleDivClicked.bind(this);
}
public handleDivClicked() {
if (this.componentClick) {
this.componentClick.emit();
this.render();
}
}
ngOnChanges(changes: SimpleChanges): void {
this.render();
}
ngAfterViewInit() {
this.render();
}
ngOnDestroy() {
ReactDOM.unmountComponentAtNode(this.containerRef.nativeElement);
}
private render() {
const {counter} = this;
ReactDOM.render(<div className={'i-am-classy'}>
<MyReactComponent counter={counter} onClick={this.handleDivClicked}/>
</div>, this.containerRef.nativeElement);
}
}
Source Code
You can find a complete working example code in this git repo.
Happy coding,
– Zacky
Leave A Comment