0

I've a pair of functional component hooks on a page, the AddVocab component adds new words to an array and ListVocab maps the array.

Everything works fine until I navigate away from the page, upon which I get this error:

react_devtools_backend.js:3973 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.

To fix this memory leak, I've tried many variations of useEffect() and cleanup function but to no avail. I know the cause is from the async VocabDataService Axios call not unsubscribing/cancelling when the components are unmounted.

Can anyone provide the correct useEffect() and cleanup function?

Here are the related files:

components/vocab/ListVocab.js

import React, { useState, useEffect } from "react";
import VocabDataService from "../../services/vocab";

export default function VocabList() {
    const [vocab, setVocab] = useState([]);
    const [currentVocab, setCurrentVocab] = useState(null);

    const retrieveVocab = () => {
        VocabDataService.getAll()
            .then((response) => {
                setVocab(response.data);
            })
            .catch((e) => {
                console.log(e);
            });
    };

    useEffect(() => retrieveVocab());

    const removeAllVocab = () => {
        VocabDataService.deleteAll()
            .then((response) => {
                setCurrentVocab(null);
            })
            .catch((e) => {
                console.log(e);
            });
    };

    return (
        <div>
            <div>
                <button onClick={removeAllVocab}>Delete</button>
                <ul>
                    {vocab.map((vocab, index) => (
                        <li key={index}>{vocab.word}</li>
                    ))}
                </ul>
            </div>
        </div>
    );
}

components/vocab/AddVocab.js

import React, { useState } from "react";
import VocabDataService from "../../services/vocab";

export default function AddVocab() {
    const [id, setId] = useState(null);
    const [word, setWord] = useState("");
    const [translation, setTranslation] = useState("");
    const [starred, setStarred] = useState(false);
    const [submitted, setSubmitted] = useState(false);

    const onChangeWord = (e) => {
        setWord(e.target.value);
    };

    const onChangeTranslation = (e) => {
        setTranslation(e.target.value);
    };

    const saveVocab = () => {
        var data = {
            word: word,
            translation: translation,
        };

        VocabDataService.create(data)
            .then((response) => {
                setId(response.data.id);
                setWord(response.data.word);
                setTranslation(response.data.translation);
                setStarred(response.data.starred);
                setSubmitted(true);
            })
            .catch((e) => {
                console.log(e);
            });
    };

    const newVocab = () => {
        setId(null);
        setWord("");
        setTranslation("");
        setStarred(false);
        setSubmitted(false);
    };

    return (
        <div className="submit-form">
            {submitted ? (
                <>{newVocab()}</>
            ) : (
                <div>
                    <div className="form-group">
                        <label htmlFor="word">Word:</label>
                        <input
                            type="text"
                            className="form-control"
                            id="word"
                            required
                            value={word}
                            onChange={onChangeWord}
                            name="word"
                        />
                    </div>

                    <div className="form-group">
                        <label htmlFor="translation">Translation:</label>
                        <input
                            type="text"
                            className="form-control"
                            id="translation"
                            required
                            value={translation}
                            onChange={onChangeTranslation}
                            name="translation"
                        />
                    </div>

                    <button onClick={saveVocab} className="btn btn-success">
                        Submit
                    </button>
                </div>
            )}
        </div>
    );
}

pages/vocab.js

import React from "react";
import Routes from "../components/Routes";
import AddVocab from "../components/vocab/AddVocab";
import VocabList from "../components/vocab/VocabList";

export default function App() {
    return (
        <div>
            <Routes />
            <div>
                <AddVocab />
                <VocabList />
            </div>
        </div>
    );
}

Thanks!

Lil Robots
  • 53
  • 1
  • 12
  • This answer may help you: https://stackoverflow.com/a/72046216/14300834 –  Apr 29 '22 at 21:40
  • `useEffect(() => retrieveVocab(), []);` add the `[]` here or it will run each time the component renders, producing a loop. – TheWuif Apr 29 '22 at 21:50
  • Maybe resetForm is triggering its setTimeout after you navigate away? – James Apr 29 '22 at 21:59
  • @LuisMartinez I've tried all these 3 variations, none worked, also when using a dependency as the second argument it prevents the list from refreshing. This happens with an empty array or with either vocab variable. ``` seEffect(() => { retrieveVocab(); }, []); ``` – Lil Robots Apr 29 '22 at 23:11
  • @TheWuif Adding the dependency prevents the list from refreshing when a new word is added. – Lil Robots Apr 29 '22 at 23:19
  • @James Thanks, I tried this but still getting the memory leak. I've updated the `components/AddVocab.js` file by removing the resetForm function and in the return JSX. Does simplify things at least. – Lil Robots Apr 29 '22 at 23:25

2 Answers2

0

useEffect has a cleanup function that sits in its return statement.

Try this:

useEffect(() => {
    let didCancel = false;
    if (!didCancel ) {
        retrieveVocab();
    }

    return() => {
        canceled = true;
    };
 });

or the same approach but just before updating the state (with the function inside the effect):

useEffect(() => {
    let didCancel = false;

    VocabDataService.getAll()
        .then((response) => {
            if (!didCancel) {
                setVocab(response.data);
            }
        })
        .catch((e) => {
            console.log(e);
        });

    return() => {
        didCancel = true;
    };
});

React will execute the function when it's time to "clean". That is, when the component is to be unmounted.

Note: If you want, you can use ref for it, instead of a simple variable.

Take a look at the React doc

I hope it works for you

Eudes Serpa
  • 114
  • 1
  • 5
0

Memory leak is happeing because you are updating a state while the component is unmounted so while setting a state just add a check if component is mounted or not.

if(mounted){
   setVocab(data);
}

This link will help you checking/setting if mounted.

Osama Malik
  • 267
  • 2
  • 6