Home > OS >  How to fix Memory Leak when setting the state in my app
How to fix Memory Leak when setting the state in my app

Time:09-24

I am currently working on a music application and am running into a memory leak error message when navigating away from the details page.

When I use the back button to navigate back to my home page I get this error message in the console.

index.js:1 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

How do I fix this? I havn't had to deal with any code cleanup in the past while working with React.

ConcertDetails.js

import React, { useState, useEffect } from 'react';
import axios from '../utils/API';

const ConcertDetails = (Props) => {

  const [details, setDetails] = useState({});
  const [songs, setSongs] = useState([]);

  useEffect(() => {
    const url = '/concerts/'   Props.match.params.id.toString()   '/';

    const getDetails = async () => {
      const response = await axios.get(url, {}, {'Content-Type': 'application/json'});
      setDetails(response.data);
      setSongs(response.data.song);
    };
    getDetails();
  });

  const getSongs = () => {
    return songs.map((song) => (
      <li key={song.title}>{song.title}</li>
    ));
  }

  return (
    <>
      <h1>Concert Details</h1>
      <p>Venue: {details.venue}</p>
      <p>Date: {details.date}</p>
      <ul>
        {getSongs()}
      </ul>
    </>
  )
}

export default ConcertDetails;

App.js

import React from 'react';
import './App.css';
import NavBar from "./components/Navbar";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Home from './pages';
import About from './pages/about';
import Contact from './pages/contact';
import SignUp from './pages/signup';
import SignIn from './pages/signin';
import ConcertDetails from './pages/concertDetails';

function App() {
  return (
    <Router>
      <NavBar />
      <Switch>
        <Route path="/" exact component={Home} />
        <Route path="/about" component={About} />
        <Route path="/contact" component={Contact} />
        <Route path="/signin" component={SignIn} />
        <Route path="/signup" component={SignUp} />
        <Route path="/concert/:id" component={ConcertDetails} />
      </Switch>
    </Router>
  );
}

export default App;

CodePudding user response:

You want to run useEffect only on first component render. Add [] as the second param of useEffect.

useEffect(() => {
    const url = '/concerts/'   Props.match.params.id.toString()   '/';

    const getDetails = async () => {
      const response = await axios.get(url, {}, {'Content-Type': 'application/json'});
      setDetails(response.data);
      setSongs(response.data.song);
    };
    getDetails();
  }, []);

without the second param. the effect will run each time state / props is changed. Since the effect changes the state, this can cause an infinite loop.

CodePudding user response:

Check the plnkr: CodeSanbox: https://codesandbox.io/s/suspicious-dew-rsz5s?file=/src/App.js

The explanation:

There are multiple issues, let's solve step by step

  • Your useEffect is running multiple times, we have to define a condition with the array of dependencies, I'll define it only runs on the mount with empty dependencies.
  • But also, your useEffect is trying to setState after an async call. So, maybe at that moment, the component is already unmounted. For that reason, you have to run a safeSetState :). Basically, you have to validate before setState.

First, we will define our hook validation of mount/unmounted:

useSafeWrite.js:

import React from 'react';

function useSafeWrite(dispatch) {
  const mounted = React.useRef(false)
  React.useLayoutEffect(() => {
    mounted.current = true
    return () => (mounted.current = false)
  }, [])
  return React.useCallback(
    (...args) => (mounted.current ? dispatch(...args) : void 0),
    [dispatch],
  )
}

export default useSafeWrite;

And after that, we will redefine the setDetails and setSongs with our hook and use them inside our useEffect.

ConcertDetails


  const [details, setDetails] = useState({});
  const [songs, setSongs] = useState([]);

  const safeSetDetails = useSafeWrite(setDetails);
  const safeSetSongs =  useSafeWrite(setSongs);

  useEffect(() => {
    const getDetails = async () => {
 const url = '/concerts/'   Props.match.params.id.toString()   '/';

       const response = await axios.get(url, {}, {'Content-Type': 'application/json'});
     
      safeSetDetails(response.data);
      safeSetSongs(response.data.song );
    };
    getDetails();
  }, [safeSetDetails, safeSetSongs]);

CodeSanbox: https://codesandbox.io/s/suspicious-dew-rsz5s?file=/src/App.js

  • Related