@memberjunction/ng-react
Version:
Angular components for hosting React components in MemberJunction applications
189 lines • 7.12 kB
JavaScript
/**
* @fileoverview Service to manage React and ReactDOM instances with proper lifecycle.
* Bridges Angular components with React rendering capabilities.
* @module @memberjunction/ng-react
*/
import { Injectable } from '@angular/core';
import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { filter } from 'rxjs/operators';
import { AngularAdapterService } from './angular-adapter.service';
import * as i0 from "@angular/core";
import * as i1 from "./angular-adapter.service";
/**
* Service to manage React and ReactDOM instances with proper lifecycle.
* Provides methods for creating and managing React roots in Angular applications.
*/
export class ReactBridgeService {
constructor(adapter) {
this.adapter = adapter;
this.reactRoots = new Set();
// Track React readiness state
this.reactReadySubject = new BehaviorSubject(false);
this.reactReady$ = this.reactReadySubject.asObservable();
// Track if this is the first component trying to use React
this.firstComponentAttempted = false;
this.maxWaitTime = 5000; // Maximum 5 seconds wait time
this.checkInterval = 200; // Check every 200ms
// Bootstrap React immediately on service initialization
this.bootstrapReact();
}
ngOnDestroy() {
this.cleanup();
}
/**
* Bootstrap React early during service initialization
*/
async bootstrapReact() {
try {
await this.adapter.initialize();
console.log('React ecosystem pre-loaded successfully');
}
catch (error) {
console.error('Failed to pre-load React ecosystem:', error);
}
}
/**
* Wait for React to be ready, with special handling for first component
*/
async waitForReactReady() {
// If already ready, return immediately
if (this.reactReadySubject.value) {
return;
}
// Check if this is the first component attempting to use React
const isFirstComponent = !this.firstComponentAttempted;
this.firstComponentAttempted = true;
if (isFirstComponent) {
// First component - check periodically until React is ready
console.log('First React component loading - checking for React initialization');
const startTime = Date.now();
while (Date.now() - startTime < this.maxWaitTime) {
try {
const testDiv = document.createElement('div');
const context = this.adapter.getRuntimeContext();
if (context.ReactDOM?.createRoot) {
// Try to create a test root
const testRoot = context.ReactDOM.createRoot(testDiv);
if (testRoot) {
testRoot.unmount();
// React is ready!
this.reactReadySubject.next(true);
console.log(`React is fully ready after ${Date.now() - startTime}ms`);
return;
}
}
}
catch (error) {
// Not ready yet, continue checking
}
// Wait before next check
await new Promise(resolve => setTimeout(resolve, this.checkInterval));
}
// If we've exhausted the wait time, throw error
console.error('React readiness test failed after maximum wait time');
this.firstComponentAttempted = false;
throw new Error(`ReactDOM.createRoot not available after ${this.maxWaitTime}ms`);
}
else {
// Subsequent components wait for the ready signal
await firstValueFrom(this.reactReady$.pipe(filter(ready => ready)));
}
}
/**
* Get the current React context if loaded
* @returns React context with React, ReactDOM, Babel, and libraries
*/
async getReactContext() {
await this.adapter.initialize();
return this.adapter.getRuntimeContext();
}
/**
* Get the current React context synchronously
* @returns React context or null if not loaded
*/
getCurrentContext() {
if (!this.adapter.isInitialized()) {
return null;
}
return this.adapter.getRuntimeContext();
}
/**
* Create a React root for rendering
* @param container - DOM element to render into
* @returns React root instance
*/
createRoot(container) {
const context = this.getCurrentContext();
if (!context?.ReactDOM?.createRoot) {
throw new Error('ReactDOM.createRoot not available');
}
const root = context.ReactDOM.createRoot(container);
this.reactRoots.add(root);
return root;
}
/**
* Unmount and clean up a React root
* @param root - React root to unmount
*/
unmountRoot(root) {
if (root && typeof root.unmount === 'function') {
try {
root.unmount();
}
catch (error) {
console.warn('Failed to unmount React root:', error);
}
}
this.reactRoots.delete(root);
}
/**
* Transpile JSX code to JavaScript
* @param code - JSX code to transpile
* @param filename - Optional filename for error messages
* @returns Transpiled JavaScript code
*/
transpileJSX(code, filename) {
return this.adapter.transpileJSX(code, filename);
}
/**
* Clean up all React roots and reset context
*/
cleanup() {
// Unmount all tracked React roots
for (const root of this.reactRoots) {
try {
root.unmount();
}
catch (error) {
console.warn('Failed to unmount React root:', error);
}
}
this.reactRoots.clear();
// Reset readiness state
this.reactReadySubject.next(false);
this.firstComponentAttempted = false;
// Clean up adapter
this.adapter.destroy();
}
/**
* Check if React is currently ready
* @returns true if React is ready
*/
isReady() {
return this.reactReadySubject.value;
}
/**
* Get the number of active React roots
* @returns Number of active roots
*/
getActiveRootsCount() {
return this.reactRoots.size;
}
static { this.ɵfac = function ReactBridgeService_Factory(t) { return new (t || ReactBridgeService)(i0.ɵɵinject(i1.AngularAdapterService)); }; }
static { this.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: ReactBridgeService, factory: ReactBridgeService.ɵfac, providedIn: 'root' }); }
}
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ReactBridgeService, [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], () => [{ type: i1.AngularAdapterService }], null); })();
//# sourceMappingURL=react-bridge.service.js.map