question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Memory Leak - React DOM keeps references to stale child components when using the Container/Pure component pattern

See original GitHub issue

Do you want to request a feature or report a bug? Report a bug.

What is the current behavior? ReactDOM keeps references to previous states/props/children when component gets updated. All in all consuming three times as much memory as it really needed.

We are seeing this as a significant issue when using Redux and container components. When our container componet(that is connected to redux store) passes props to the child components, and then the redux store updates. The child component props are being stranded in the dom with old reference(seen in the heap after forcing a garbage collection cycle). This is causing huge amounts of memory bloat when on a page that is connected to signalR for real time collaboration between users(as each redux update creates hundreds of stranded object references in the child components).

I have verified that this is the cause by instead having all of the previously “dumb” pure child components be converted to Connected components and pull their props from redux instead of having the container component pattern control all of the store connections. this then correctly all references the single redux store object and garbage collection works as expected without stranded references.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn’t have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:

Link to the example below (using production versions of react and react-dom): https://codesandbox.io/s/epic-bartik-pvgqx.

Consider following example:

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

let dataInstanceCount = 0;
class MyBigData {
  constructor() {
    const id = `my-big-data:${dataInstanceCount++}`;
    this.getMyDataId = () => id;
    this.data = new Array(100000).fill('');
  }
}

let componentInstanceCount = 0;
class MyItem extends React.Component {
  constructor(props) {
    super(props);
    this._myItemId = `my-item:${componentInstanceCount++}`;
    this.state = {list: []};
  }

  render() {
    return this.props.item.getMyDataId();
  }
}

class MyApp extends React.Component {
  constructor(props) {
    super(props);
    this.state = {list: []};
  }

  componentDidMount() {
    this.updateList(() => {
      this.updateList(() => {
        this.updateList();
      });
    });
  }

  updateList(callback) {
    this.setState({
      list: [new MyBigData()]
    }, callback);
  }

  render() {
    return this.state.list.map((item) => (
      <MyItem key={item.getMyDataId()} item={item} />
    ));
  }
}

const rootElement = document.getElementById('root');
ReactDOM.render(
  <MyApp />,
  rootElement
);

setTimeout(() => {
  console.log(
    rootElement._reactRootContainer._internalRoot.current.alternate.firstEffect.memoizedProps.item.getMyDataId(),
    rootElement._reactRootContainer._internalRoot.current.alternate.firstEffect.stateNode._myItemId
  );
  // > my-big-data:0, my-item:0
  console.log(
    rootElement._reactRootContainer._internalRoot.current.firstEffect.memoizedProps.item.getMyDataId(),
    rootElement._reactRootContainer._internalRoot.current.firstEffect.stateNode._myItemId
  );
  // > my-big-data:1, my-item:1
  console.log(
    rootElement._reactRootContainer._internalRoot.current.lastEffect.memoizedProps.item.getMyDataId(),
    rootElement._reactRootContainer._internalRoot.current.lastEffect.stateNode._myItemId
  );
  // > my-big-data:2, my-item:2
}, 1000);

I expect only one MyBigObject and one MyItem component to be in the memory. But instead I can see three of each in memory heap snapshot.

UPDATE As shown in the updated example the references to these objects and components can be accessed in the sub-properties of the root DOM element.

What is the expected behavior? There’s no justifiable reason to keep in memory unmounted components and previous states/props of component after it was updated.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React? React 16.9.0, ReactDOM 16.9.0 (Production versions) Mac/Win

This info was gathered from the follow issue that was marked stale, but is still definitely an issue with Redux and passing store props to Pure Child unconnected components: https://github.com/facebook/react/issues/16138

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:25
  • Comments:19 (6 by maintainers)

github_iconTop GitHub Comments

12reactions
Venryxcommented, Nov 12, 2020

I have reproduced this memory leak in a very small and self-contained demo.

Demo source: https://codesandbox.io/s/reactjs-useeffect-memory-leak-8iv16?file=/src/index.tsx Demo standalone page: https://8iv16.csb.app

Leak reproduction instructions:

  1. Open the demo standalone page. (needed for consistent memory profiler data)
  2. Open dev-tools, and go to Memory tab.
  3. Press the garbage-can icon at top-left, to run GC. Memory-usage shown for “https://8iv16.csb.app” VM is the baseline. (it can vary, eg. having the demo-editor open in another tab increases it by ~50mb)
  4. Switch render-type to “Heavy”, and run GC. VM usage will increase to baseline + 60mb (1 array copy).
  5. Disable “Show contents”, and run GC. VM usage will go back down to baseline. (0 array copies) [repeat steps 4 and 5 if desired]
  6. Ensure “Show contents” is disabled, switch render-type to “Container->heavy”, and run GC. VM usage should remain at baseline. (0 array copies)
  7. Enable “Show contents”, and run GC. VM usage will increase to baseline + 60mb. (1 array copy - no leak yet)
  8. Disable “Show contents”, and run GC. VM usage will stay at baseline + 60mb! (1 array copy - leak!)
  9. Enable “Show contents”, and run GC. VM usage will increase to baseline + 120mb! (2 array copies - leak!) [repeat steps 8 and 9 if desired]

Here is a screen capture showing me performing the steps above, with exactly the results described:

The issue seems to be that – for certain patterns of component trees (eg. Container->HeavyChild in demo, but not HeavyChild directly) – React holds onto the reference to the “cleanup function” returned by the useEffect hook, even after the earlier component instance has been unmounted!

(You can confirm that the “cleanup function” of the useEffect hook is the leak-path specifically, because if you comment out that cleanup-function, the problem of encountering the “2 array copies” state is resolved. In fact, you don’t even encounter the “1 array copy” state anymore [after running GC], as nothing ever forms a func-external reference to the array.)

While the memory leak’s effects are not that serious in this demo, it can have major memory-usage ramifications in cases where the “cleanup function” of the useEffect hook references variables holding larger amounts of data. In my app specifically, the cleanup function held a reference to a very large data-array that was sent into the component. Even after the component was unmounted (/remounted with a new data-array), the useEffect hook’s “cleanup function” was holding onto the old array values!

This was causing major memory issues for my app, as the data-array was so large that holding even just a couple more than expected, was putting the app in range of out-of-memory errors. (criticize this as you like, but it’s not easy storing 256+256+64+64+64 number values per second, for an entire night, in frontend memory!)

Anyway, the point is not my specific app’s issues, but the fact that the memory leak exists in general.

2reactions
AndyJinSScommented, Oct 19, 2021

This issue has been fixed in the new version(16.13(+), 17+).

Read more comments on GitHub >

github_iconTop Results From Across the Web

patternparseexception: no more pattern data allowed after ...
facebook/reactMemory Leak - React DOM keeps references to stale child components when using the Container/Pure component pattern#18790.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found