Search code examples
reactjsangularbundlewrapper

Production build of Angular 7 with React component rendered within


I'm migrating an existing Angular app to React piece by piece. Looked at a number of options including single-spa and nx, but they're not feasible because the existing app is a horrible pile of script-injected madness. I've opted for the semi-manual option as shown by Zacky Pickholz:

https://medium.com/@zacky_14189/embedding-react-components-in-angular-the-easy-way-60f796b68aef

The project is successfully rendering a React component inside the Angular 7 app (yay!). Dev build works fine, however the production build throws up at the tsx wrapper.

ERROR in ./src/app/components/my-wrapper-rct/my-wrapper-rct.tsx
Module parse failed: Unexpected character '@' (21:2)
You may need an appropriate loader to handle this file type.
|   const containerElementName = 'myWrapperRct';
|
|   @Component({
|     selector: 'my-wrapper-rct',
|     template: `<span #${containerElementName}></span>`,

The wrapper looks like the example from Zacky's repo:
https://github.com/zackypick/react-in-angular

It looks like the production transpiler is ignoring the tsx wrapper, so when the JS encounters @Component it has no idea what to do with it. The weird thing is that the bundles seem to transpile and generate prior to this error. Hoping someone has some insight into the production build error.

Wrapper code:

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);
  }
}

Solution

  • Searched the ends of the earth and didn't find anyone facing the exact same error. This reported issue clued me in on some paths to take:

    https://github.com/angular/angular-cli/issues/9244

    After some experimentation, I needed to set aot and buildOptimizer to false. They're unable to deal with this situation.

    The build size difference was minimal: 8.3MB vs 8.4MB. Since the goal is to jettison Angular ASAP, I can live with this small increase.