Description
Do you want to request a feature or report a bug?
Bug - maybe intended behaviour.
What is the current behavior?
Background
I have an app that needs to be embedded by other apps (other customers). The idea being "our" react app has its javascript loaded in an iframe, but the "main" window hosts dom elements from the customers and our react app. That bit works fine. As time goes on "our" react UI is no longer needed, and then react root is removed, and the iframe destroyed. These apps are often long lived so there will be times when the react app needs to appear again, and the iframe is recreated and everything reloaded. This can and will happen many times.
Goal
We would like to NOT keep the iframe around when its not actually needed, but rather re-create just in time when it is needed. This app is used by customers and they would like to embed our "react" app, without interference with their "app" and all its javascript, which is why we are doing the iframe thing.
Problem
It is evident by watching the chrome dev tools "timeline" memory graph that memory always increases each time a new iframe is created and the react UI is init'd. Unmounting and destroying the iframe, never causes the memory to drop to "near" original before load value. Repeating this process multiple times slowly show an increase memory.
This also causes a more immediate problem, in that react is throwing exceptions on every event (click, type etc) because the window of the iframe is now null.
Proof: First symptom - Event exceptions (only happens in my app)
These exceptions only happen in my (cant share) app, i cant repo them, but parts of this apply to all react apps. Please read thru - it will all make sense when you get to the end and if you examine my poc.
Destroying the Iframe, leaves React and its event dispatching system in memory. I have a mixture of x-tag, webcomponents which are used to "create" the iframe and load the react app. After the custom element is used (lets call it ), the console starts showing exceptions all within react code. This is a side effect of the react dispatchEvent still being active and trying to do stuff.
Uncaught TypeError: Cannot read property 'nodeName' of null
shouldUseChangeEvent @ VM1068_embeddedApp.js:14296
extractEvents @ VM1068_embeddedApp.js:14536
extractEvents @ VM1068_embeddedApp.js:13000
handleTopLevel @ VM1068_embeddedApp.js:19816
handleTopLevelImpl @ VM1068_embeddedApp.js:23870
perform @ VM1068_embeddedApp.js:15510
batchedUpdates @ VM1068_embeddedApp.js:23787
batchedUpdates @ VM1068_embeddedApp.js:14673
dispatchEvent @ VM1068_embeddedApp.js:23946
I know about ReactEventListener.dispatchEvent
(snip below) where i can disable react( i havent actually tried) to avoid the exceptions, but that would leave the memory leak.
https://github.com/facebook/react/blob/master/src/renderers/dom/client/ReactEventListener.js#158
dispatchEvent: function(topLevelType, nativeEvent) {
if (!ReactEventListener._enabled) {
return;
}
Its rather easy to prove that react remains in memory, simply goto the compiled app, find the React dispatchEvent
and insert a console.log and watch as it continues to "print" stuff after unmounting the last component, even though there are no listeners. In my case the exception is caused because all extractEvents
eventually default to "window" as the "target".
There are multiple copies of the same basic idea in various react functions, where it tries to get a target that it assumes will never be null. If one doesnt load react in an iframe, then window is always defined.
var targetNode = targetInst ?
ReactDOMComponentTree.getNodeFromInstance(targetInst) : window;
Later the shouldUseChangeEvent
tries to read the nodeName of the now "undefined" window, because its iframe has been destroyed, but that now results in an exception (null pointer etc).
function shouldUseChangeEvent(elem) {
var nodeName = elem.nodeName && elem.nodeName.toLowerCase();
return nodeName === 'select' || nodeName === 'input' && elem.type === 'file';
}
If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://jsfiddle.net or similar (template: https://jsfiddle.net/reactjs/69z2wepo/).
What is the expected behavior?
There are probably two possible solutions, that work in tandem.
-
Firstly React should provide an API that will remove all its global event listeners. Naturally it could complain if there are any active components that remain mounted. This API may be internal/private (not public), if [docs] Fix button links on bottom of home #2 was implemented. It might be called something like
React.shutdownAll
Because everything is gone, the next React render would setup all its globals again. -
React should dispose of all its global event handlers when the last or "root" component is unmounted. This would call the new api mentioned in 1.
Either option solves my problem, where i wish to either let react shutdown gracefully. With this in mind i could.
- unmount iframe powered react ui component.
- call React.disposeGlobals (mentioned above). If unmounting auto calls an internal
React.shutdownAll
then this step is skipped. - destroy iframe.
Proof #2
Goto your compiled out, locate the dispatchEvent
and add a console.log, notice even after the last / root container is unmounted stuff will continue to be printed because the event listeners are still active.
I did a very quick scan of the abstraction around adding listeners, and i couldnt see the remove function being stored and then called to cleanup.
Proof #3
Look at my last section below where i have a proof of concept form of the popular todomvc react example.
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
React 15.0.2
React-Dom 15.0.2
React-redux 4.4.5 (might be useful to know)
Reproducable use case
Sorry i tried but decided that using the facebook jsfiddle wasnt really a smart thing for the following reasons.
- the compile the "jsx" content means loading babel etc to compile (babel, jsfiddle etc too many moving parts)
- its "hard" to get the "root component" that is inserted into the "output" box and
- its even just too "hard" to put the jsx compiled output into somewhere for the iframe src= to "load".
I have forked the popular todomvc app and added a few minor edits to recreate, reload, render+unmount x100, destroy everything about the app, and try again in a loop separated by a sleep.
- https://todomvc.com (todomvc main site)
- https://github.com/tastejs/todomvc (todomvc github)
- https://github.com/mP1/todomvc/pull/2 (my fork - with comments and snapshots of chrome dev tools timeline memory graph)
Hopefully we can trust the todomvc guys are doing the right thing, no dumb memory leaks. If you examine it should be obvious the only thing im adding is support for my horrible create app, run app, render+unmount many times, render, unmount, sleep a bit and then loop again until counter exhausted.
Sorry if this is boring but as a convenience i will list the basic instructions to "run" the react version of my branch on your local machines...
- clone https://github.com/tastejs/todomvc.git
- in the root, run "gulp", to compile everything.
- run something like "python -m SimpleHTTPServer"
4A. navigate to http://localhost:8000/examples/react/index.html
4B. navigate to http://localhost:8000/examples/react/index3.html
// /examples/react corresponds to the dist/examples/react directory that gulp built into.
My poc supports 3 concepts.
- re-run todomvc over and over again in "same" window.
- create iframe, load todomvc js in the iframe but render to outer window, unmount, destroy iframe, try 20x
- create custom element, webcontainer creates iframe and load todomvc js in the iframe but render to outer window, unmount, destroy custom element, try 20x
If you look at my p/r against todomvc you will see many helpful pictures with memory leak graphs from chrome dev tools for each of the 3 described scenarios and some commentary.