How ToT2 Dark_1200x303

Memory leaks can cause an application to run slow or even crash. They can be created by accident, by not cleaning up after yourself. Find out how to clean up event listeners in the two very popular JavaScript frameworks Vue and React.

I work with a lot of clients, from beginners to senior developers, but from time to time I spot that their code is missing something. Unfortunately, they do not clean up after themselves, and I don’t mean their dishes. I’m talking about cleaning up event listeners which, if left, could cause memory leaks.

In small applications it might not even be visible. In larger ones, this can cause various problems like a slow and laggy experience, or even a crash. Therefore, this article will explain how we can clean up after ourselves in very popular JavaScript frameworks—Vue and React.

What Is the Cause of the Problem?

Let’s start with a simple example. Imagine an app with a list of items. This list is replaced by a list of new items every few seconds. The code below illustrates the problem and will allow you to see it.

App component

Every few seconds, createItems function is called to create new items, which are then looped through. index and id are passed to the Item component as props.

Item component

The Item component renders current item’s id and index. Moreover, when the component is created, a click event listener is added that will log out item’s id and index.

Vue

App.vue

<template>
  <div id="app">
    <Item v-for="(item, i) of items" :index="i" :id="item.id" :key="item.id" />  
  </div>
</template>

<script>
import Item from "./components/Item";

const createItems = () =>
  Array.from({ length: 10 }).map(() => ({
    id: Math.random()
      .toString(36)
      .substr(2, 9)
  }));

export default {
  components: {
    Item
  },
  data() {
    return {
      items: createItems()
    };
  },
  created() {
    setInterval (() => {
      this.items = createItems();
    }, 5000);
  }
};
</script>

<style>
</style>

Item.vue

<template>
  <div>Item {{id}} - {{index}}</div>
</template>
<script>
export default {
  props: {
    index: Number,
    id: String
  },
  created() {
    window.addEventListener("click", () => {
      console.log(`${this.id} - ${this.index}`);
    });
  }
};
</script>

React

App.js

import React, { useState, useEffect } from 'react';
import Item from './Item';

const createItems = () =>
  Array.from({ length: 10 }).map(() => ({
    id: Math.random()
      .toString(36)
      .substr(2, 9),
  }));

function App() {
  const [items, setItems] = useState(createItems());

  useEffect(() => {
    setInterval(() => {
      setItems(createItems());
    }, 5000);
  }, []);

  return (
    <div className='App'>
      {items.map((item, i) => {
        return <Item id={item.id} index={i} key={item.id} />;
      })}
    </div>
  );
}

export default App;

Item.js

import React, { useEffect } from 'react';

const Item = props => {
  const { id, index } = props;

  useEffect(() => {
    const onClick = () => {
      console.log(`${id} - ${index}`);
    };
    // Add on click event listener
    window.addEventListener('click', onClick);
  }, []);
  return (
    <div>
      Item {id} - {index}
    </div>
  );
};

export default Item;

Now, open developer tools and go to the Console tab. Refresh the website, and immediately click anywhere on the page. You should see 10 items logged out.

However, wait for a few seconds for the items to change and click again. Are you surprised that you can see 20 logs, instead of 10, even though we still have 10 items? The reason is that the components were recreated for the new items, as they have different ids. For each item we provide the key prop, which is used by Vue and React to determine if an item should be updated or recreated.

In any case, wait a bit longer and click again. At some point we have a few hundred listeners, and most of them are for items that no longer exist. Fortunately, this can be easily fixed.

The Solution

We need to make sure to clean up after ourselves when a component is being destroyed. Update your code as shown below.

Vue

In Vue we can listen for the hook:beforeDestroy event on component instance, and pass a callback which will remove the event listener.

created() {
    // Create onClick function
    const onClick = () => {
      console.log(`${this.id} - ${this.index}`);
    };
    // Add on click event listener
    window.addEventListener("click", onClick);

    // Remove the event listener on beforeDestroy hook
    this.$on("hook:beforeDestroy", () =>
      window.removeEventListener("click", onClick)
    );
  }

You can also define the beforeDestroy lifecycle hook on the component instance, but you will also need to move the onClick handler to methods.

methods: {
    onClick() {
      console.log(`${this.id} - ${this.index}`);
    }
  },
  beforeDestroy() {
    window.removeEventListener("click", this.onClick);
  },
  created() {
    window.addEventListener("click", this.onClick);
  }

React

In React, just return a function from the useEffect hook and remove the event listener there.

useEffect(() => {
    const onClick = () => {
      console.log(`${id} - ${index}`);
    };
    // Add on click event listener
    window.addEventListener('click', onClick);

    return () => {
      window.removeEventListener('click', onClick);
    };
  }, []);

If you click anywhere on the screen and check the console again, you will see that there are only 10 logs. The memory leak was successfully fixed.

Conclusion

This example used an event listener, but the same problem can happen if you forget to clean up after third-party libraries. Some libraries might create their own event listeners and they require you to explicitly call a method to clean up.

I hope you found this article useful and that you will now make sure to always clean up after yourself. You can find full code in this GitHub repository: https://github.com/ThomasFindlay/cleanup-after-yourself.


Thomas Findlay-2
About the Author

Thomas Findlay

Thomas Findlay is a 5-star rated mentor, full-stack developer, consultant, technical writer and the author of “Vue - The Road To Enterprise.” He works with many different technologies such as JavaScript, Vue, React, React Native, Node.js, Python, PHP and more. Thomas has worked with developers and teams from beginner to advanced and helped them build and scale their applications and products. Check out his Codementor page, and you can also find him on Twitter.

Related Posts

Comments

Comments are disabled in preview mode.