4

So basically I want to render one single React component (a notification component) into my Angular project. I created Notification.tsx

import * as React from 'react';
import { FunctionComponent, useEffect, useRef, useState } from 'react';
import { IconButton, Badge, Menu, List, ListItem, ListItemIcon, ListItemText, ListItemSecondaryAction, Avatar } from '@material-ui/core';
import { Notifications, Delete, Event, HourglassEmpty, Alarm } from '@material-ui/icons';
import { makeStyles } from '@material-ui/core/styles';
import { grey } from '@material-ui/core/colors';

export interface NotificationProps { }

export const NotificationComponent: FunctionComponent<NotificationProps> = (props: NotificationProps) => {
    return <div className={"row"}>
        <IconButton disableRipple={true} aria-label="notification" className="py-3 my-auto"
            onClick={handleNotificationMenuClick} aria-controls="notification-drop-down-menu"
            aria-haspopup="true">
            <Badge badgeContent={4} max={99} color="secondary">
                <Notifications />
            </Badge>
        </IconButton>
    </div>
};

A wrapper component NotificationWrapper.tsx

import { AfterViewInit, Component, ElementRef, OnChanges, OnDestroy, SimpleChanges, ViewChild, ViewEncapsulation } from '@angular/core';
import { NotificationComponent } from './Notification';
import * as React from 'react';
import * as ReactDOM from 'react-dom';

const containerElementName = 'notificationComponentTemplate';

@Component({
    selector: 'react-notification-component',
    template: `<span #${containerElementName}></span>`,
    encapsulation: ViewEncapsulation.None,
})
export class NotificationWrapper implements OnChanges, OnDestroy, AfterViewInit {
    @ViewChild(containerElementName, { static: false }) containerRef: ElementRef;

    constructor() { }

    ngOnChanges(changes: SimpleChanges): void {
        this.render();
    }

    ngAfterViewInit() {
        this.render();
    }

    ngOnDestroy() {
        ReactDOM.unmountComponentAtNode(this.containerRef.nativeElement);
    }

    private render() {
        ReactDOM.render(<div className={'notification-wrapper'}>
            <NotificationComponent />
        </div>, this.containerRef.nativeElement);
    }
}

Added this wrapper to app.module.ts's @NgModule

import { NotificationWrapper } from "../react-components/notification/NotificationWrapper";

@NgModule({
  declarations: [
    NotificationWrapper,
  ],
})

Used the notification wrapper selector as follows:

        <div class="col-sm-6">
          <react-notification-component></react-notification-component>
        </div>

Everything works fine when served locally and as I went through other similar questions on this site: I've added "jsx": "react" to tsconfig.json,

  plugins: [
    new webpack.ProvidePlugin({
      "React": "react",
    })
  ]

and

  externals: {
    'react': 'React'
  },

to webpack.config.js. Here's the whole file for reference.

// Work around for https://github.com/angular/angular-cli/issues/7200

const path = require('path');
const webpack = require('webpack');
// change the regex to include the packages you want to exclude
const regex = /firebase\/(app|firestore)/;

module.exports = {
  mode: 'production',
  entry: {
    // This is our Express server for Dynamic universal
    server: './server.ts'
  },
  externals: {
    './dist/server/main': 'require("./server/main")',
    'react': 'React'
  },
  target: 'node',
  node: {
    __dirname: false,
    __filename: false,
  },
  resolve: { extensions: ['.ts', '.js'] },
  target: 'node',
  mode: 'none',
  // this makes sure we include node_modules and other 3rd party libraries
  externals: [/node_modules/, function (context, request, callback) {
    // exclude firebase products from being bundled, so they will be loaded using require() at runtime.
    if (regex.test(request)) {
      return callback(null, 'commonjs ' + request);
    }
    callback();
  }],
  optimization: {
    minimize: false
  },
  output: {
    // Puts the output at the root of the dist folder
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  },
  module: {
    noParse: /polyfills-.*\.js/,
    rules: [
      { test: /\.ts$/, loader: 'ts-loader' },
      {
        // Mark files inside `@angular/core` as using SystemJS style dynamic imports.
        // Removing this will cause deprecation warnings to appear.
        test: /(\\|\/)@angular(\\|\/)core(\\|\/).+\.js$/,
        parser: { system: true },
      },
    ]
  },
  plugins: [
    new webpack.ProvidePlugin({
      "React": "react",
    }),
    new webpack.ContextReplacementPlugin(
      // fixes WARNING Critical dependency: the request of a dependency is an expression
      /(.+)?angular(\\|\/)core(.+)?/,
      path.join(__dirname, 'src'), // location of your src
      {} // a map of your routes
    ),
    new webpack.ContextReplacementPlugin(
      // fixes WARNING Critical dependency: the request of a dependency is an expression
      /(.+)?express(\\|\/)(.+)?/,
      path.join(__dirname, 'src'),
      {}
    )
  ]
};

My problem is, when I build my app, the notification component is not rendered and I get this error in console ERROR ReferenceError: React is not defined. Is there anything that I missed? Thanks in advance.

2 Answers 2

7

I think it's a typical unwanted optimization that remove React in the final js (React isn't directly used)

Try to call a method or an attribute on React like React.version :

import {
  OnChanges,
  ViewChild
} from '@angular/core';
import { MyComponent } from './react/MyComponent';
import * as React from 'react';
import * as ReactDOM from 'react-dom';

const containerElementName = 'myReactComponentContainer';

@Component({
  ...
})
export class MyComponent implements OnChanges {

  @ViewChild(containerElementName) containerRef: ElementRef;
  ngOnChanges(changes: SimpleChanges): void {
    this.render();
  }

  private render() {
    React.version;
    ReactDOM.render(<MyReactComponent />,     this.containerRef.nativeElement);
  }
}
Sign up to request clarification or add additional context in comments.

1 Comment

This one solved my issue and didn't have to change this Set aot and buildOptimizer to false in angular.json
6

Add the following to tsconfig.json

"compilerOptions": {
  "allowSyntheticDefaultImports": true,
  "jsx": "react",
}

Set aot and buildOptimizer to false in angular.json

Replace

import * as React from 'react';
import * as ReactDOM from 'react-dom'

with

import React from 'react';
import ReactDOM from 'react-dom';

3 Comments

Coming across the same problem, tho this solution didnt work for me, only way I can get it to work is by setting "optimization" in angular.json to false. But not something I really want to do :(
set aot and buildOptimizer to false
I did, you already said that in your solution, upgrading Angular 11 from 10 worked with your solution in my case

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.