From a339e6cb94013abe35e81a0f8d265c7af53cae67 Mon Sep 17 00:00:00 2001 From: ZvozdaB Date: Wed, 4 Mar 2026 14:47:22 +0200 Subject: [PATCH 1/2] fix: prevent root.render() after root.unmount() when $onChanges fires after $onDestroy Add _destroyed flag in $onDestroy and guard $doCheck/$onChanges with early return to avoid "Cannot update an unmounted root" error in React 19 when AngularJS fires $onChanges after $onDestroy in the same digest cycle. Bump version to 18.0.1. Made-with: Cursor --- index.js | 2 +- package.json | 2 +- src/index.js | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 58c64dc..e71adef 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -"use strict";var React=require("react");var ReactDOMClient=require("react-dom/client");var isPlainObject=require("lodash/isPlainObject");var isEqual=require("lodash/isEqual");function angularize(Component,componentName,angularApp,bindings){bindings=bindings||{};if(typeof window==="undefined"||typeof angularApp==="undefined")return;angularApp.component(componentName,{bindings:bindings,controller:["$element",function($element){var _this=this;this.root=ReactDOMClient.createRoot($element[0]);if(window.angular){this.$scope=window.angular.element($element).scope();var previous={};this.$onInit=function(){for(var _i=0,_Object$keys=Object.keys(bindings);_i<_Object$keys.length;_i++){var bindingKey=_Object$keys[_i];if(/^data[A-Z]/.test(bindingKey)){console.warn("'".concat(bindingKey,"' binding for ").concat(componentName," component will be undefined because AngularJS ignores attributes starting with data-"))}if(bindings[bindingKey]==="="){previous[bindingKey]=window.angular.copy(_this[bindingKey])}}};this.$doCheck=function(){for(var _i2=0,_Object$keys2=Object.keys(previous);_i2<_Object$keys2.length;_i2++){var previousKey=_Object$keys2[_i2];if(!equals(_this[previousKey],previous[previousKey])){_this.$onChanges();previous[previousKey]=window.angular.copy(_this[previousKey]);return}}}}this.$onChanges=function(){_this.root.render(React.createElement(Component,_this))};this.$onDestroy=function(){_this.root.unmount()}}]})}function angularizeDirective(Component,directiveName,angularApp,bindings){bindings=bindings||{};if(typeof window==="undefined"||typeof angularApp==="undefined")return;angularApp.directive(directiveName,function(){return{scope:bindings,replace:true,link:function link(scope,element){scope.$scope=scope;var root=ReactDOMClient.createRoot($element[0]);root.render(React.createElement(Component,scope));var keys=[];for(var _i3=0,_Object$keys3=Object.keys(bindings);_i3<_Object$keys3.length;_i3++){var bindingKey=_Object$keys3[_i3];if(/^data[A-Z]/.test(bindingKey)){console.warn("\"".concat(bindingKey,"\" binding for ").concat(directiveName," directive will be undefined because AngularJS ignores attributes starting with data-"))}if(bindings[bindingKey]!=="&"){keys.push(bindingKey)}}scope.$watchGroup(keys,function(root){root.render(React.createElement(Component,scope))});scope.$on("$destroy",function(){root.unmount()})}}})}function getService(serviceName){if(typeof window==="undefined"||typeof window.angular==="undefined")return{};return window.angular.element(document.body).injector().get(serviceName)}function equals(o1,o2){if(isPlainObject(o1)&&isPlainObject(o2)){return isEqual(o1,o2)}return window.angular.equals(o1,o2)}module.exports={getService:getService,angularize:angularize,angularizeDirective:angularizeDirective}; \ No newline at end of file +"use strict";var React=require("react");var ReactDOMClient=require("react-dom/client");var isPlainObject=require("lodash/isPlainObject");var isEqual=require("lodash/isEqual");function angularize(Component,componentName,angularApp,bindings){bindings=bindings||{};if(typeof window==="undefined"||typeof angularApp==="undefined")return;angularApp.component(componentName,{bindings:bindings,controller:["$element",function($element){var _this=this;this.root=ReactDOMClient.createRoot($element[0]);if(window.angular){this.$scope=window.angular.element($element).scope();var previous={};this.$onInit=function(){for(var _i=0,_Object$keys=Object.keys(bindings);_i<_Object$keys.length;_i++){var bindingKey=_Object$keys[_i];if(/^data[A-Z]/.test(bindingKey)){console.warn("'".concat(bindingKey,"' binding for ").concat(componentName," component will be undefined because AngularJS ignores attributes starting with data-"))}if(bindings[bindingKey]==="="){previous[bindingKey]=window.angular.copy(_this[bindingKey])}}};this.$doCheck=function(){if(_this._destroyed)return;for(var _i2=0,_Object$keys2=Object.keys(previous);_i2<_Object$keys2.length;_i2++){var previousKey=_Object$keys2[_i2];if(!equals(_this[previousKey],previous[previousKey])){_this.$onChanges();previous[previousKey]=window.angular.copy(_this[previousKey]);return}}}}this.$onChanges=function(){if(_this._destroyed)return;_this.root.render(React.createElement(Component,_this))};this.$onDestroy=function(){_this._destroyed=true;_this.root.unmount()}}]})}function angularizeDirective(Component,directiveName,angularApp,bindings){bindings=bindings||{};if(typeof window==="undefined"||typeof angularApp==="undefined")return;angularApp.directive(directiveName,function(){return{scope:bindings,replace:true,link:function link(scope,element){scope.$scope=scope;var root=ReactDOMClient.createRoot($element[0]);root.render(React.createElement(Component,scope));var keys=[];for(var _i3=0,_Object$keys3=Object.keys(bindings);_i3<_Object$keys3.length;_i3++){var bindingKey=_Object$keys3[_i3];if(/^data[A-Z]/.test(bindingKey)){console.warn("\"".concat(bindingKey,"\" binding for ").concat(directiveName," directive will be undefined because AngularJS ignores attributes starting with data-"))}if(bindings[bindingKey]!=="&"){keys.push(bindingKey)}}scope.$watchGroup(keys,function(root){root.render(React.createElement(Component,scope))});scope.$on("$destroy",function(){root.unmount()})}}})}function getService(serviceName){if(typeof window==="undefined"||typeof window.angular==="undefined")return{};return window.angular.element(document.body).injector().get(serviceName)}function equals(o1,o2){if(isPlainObject(o1)&&isPlainObject(o2)){return isEqual(o1,o2)}return window.angular.equals(o1,o2)}module.exports={getService:getService,angularize:angularize,angularizeDirective:angularizeDirective}; \ No newline at end of file diff --git a/package.json b/package.json index 4756772..fdef37b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-in-angularjs", - "version": "18.0.0", + "version": "18.0.1", "description": "A super simple way to render React components in AngularJS", "main": "index.js", "author": "Joshua Prodahl", diff --git a/src/index.js b/src/index.js index 985a02b..dc941dc 100644 --- a/src/index.js +++ b/src/index.js @@ -39,6 +39,7 @@ function angularize(Component, componentName, angularApp, bindings) { }; this.$doCheck = () => { + if (this._destroyed) return; for (let previousKey of Object.keys(previous)) { if (!equals(this[previousKey], previous[previousKey])) { this.$onChanges(); @@ -50,10 +51,12 @@ function angularize(Component, componentName, angularApp, bindings) { } this.$onChanges = () => { + if (this._destroyed) return; this.root.render(React.createElement(Component, this)); }; this.$onDestroy = () => { + this._destroyed = true; this.root.unmount(); }; }, From a165d019db0157b3840729c427f59e7c145cae8e Mon Sep 17 00:00:00 2001 From: ZvozdaB Date: Wed, 4 Mar 2026 15:20:37 +0200 Subject: [PATCH 2/2] fix: improve TypeScript declarations with generic type safety Replace incorrect React.Element with ComponentType, add generic bindings constraint to map component props to AngularJS binding types. Made-with: Cursor --- index.d.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index 3d4b434..bf5e9d1 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,9 +1,19 @@ declare module 'react-in-angularjs' { - import React from "react"; + import { ComponentType } from 'react'; - function getService(name: string): any; + function angularize( + Component: ComponentType, + componentName: string, + angularApp: any, + bindings: { [K in keyof TProps]?: '<' | '=' | '@' | '&' }, + ): void; - function angularize(Component: React.Element, componentName: string, angularApp: any, bindings: any): void; + function angularizeDirective( + Component: ComponentType, + directiveName: string, + angularApp: any, + bindings: { [K in keyof TProps]?: '<' | '=' | '@' | '&' }, + ): void; - function angularizeDirective(Component: React.Element, directiveName: string, angularApp: any, bindings: any): void; + function getService(serviceName: string): any; }