UNPKG

@memberjunction/ng-react

Version:

Angular components for hosting React components in MemberJunction applications

189 lines 7.12 kB
/** * @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