react hooks useEffect() cleanup for only componentWillUnmount?

ReactjsReact Hooks

Reactjs Problem Overview


Let me explain the result of this code for asking my issue easily.

const ForExample = () => {
	const [name, setName] = useState('');
	const [username, setUsername] = useState('');

	useEffect(() => {
		console.log('effect');
		console.log({
			name,
            username
		});

		return () => {
			console.log('cleaned up');
			console.log({
				name,
				username
			});
		};
	}, [username]);

	const handleName = e => {
		const { value } = e.target;

		setName(value);
	};

	const handleUsername = e => {
		const { value } = e.target;

		setUsername(value);
	};

	return (
		<div>
			<div>
				<input value={name} onChange={handleName} />
				<input value={username} onChange={handleUsername} />
			</div>
			<div>
				<div>
					<span>{name}</span>
				</div>
				<div>
					<span>{username}</span>
				</div>
			</div>
		</div>
	);
};

When the ForExample component mounts, 'effect' will be logged. This is related to the componentDidMount().

And whenever I change name input, both 'effect' and 'cleaned up' will be logged. Vice versa, no message will be logged whenever I change username input since I added [username] to the second parameter of useEffect(). This is related to the componentDidUpdate()

Lastly, when the ForExample component unmounts, 'cleaned up' will be logged. This is related to the componentWillUnmount().

We all know that.

To sum, 'cleaned up' is invoked whenever the component is being re-rendered(includes unmount)

If I want to make this component to log 'cleaned up' for only the moment when it is unmount, I just have to change the second parameter of useEffect() to [].

But If I change [username] to [], ForExample component no longer implements the componentDidUpdate() for name input.

What I want to do is that, to make the component supports both componentDidUpdate() only for name input and componentWillUnmount(). (logging 'cleaned up' for only the moment when the component is being unmounted)

Reactjs Solutions


Solution 1 - Reactjs

You can use more than one useEffect().

For example, if my variable is data1, I can use all of this in my component:

useEffect( () => console.log("mount"), [] );
useEffect( () => console.log("data1 update"), [ data1 ] );
useEffect( () => console.log("any update") );
useEffect( () => () => console.log("data1 update or unmount"), [ data1 ] );
useEffect( () => () => console.log("unmount"), [] );

Solution 2 - Reactjs

Since the cleanup is not dependent on the username, you could put the cleanup in a separate useEffect that is given an empty array as second argument.

Example

const { useState, useEffect } = React;

const ForExample = () => {
  const [name, setName] = useState("");
  const [username, setUsername] = useState("");

  useEffect(
    () => {
      console.log("effect");
    },
    [username]
  );

  useEffect(() => {
    return () => {
      console.log("cleaned up");
    };
  }, []);

  const handleName = e => {
    const { value } = e.target;

    setName(value);
  };

  const handleUsername = e => {
    const { value } = e.target;

    setUsername(value);
  };

  return (
    <div>
      <div>
        <input value={name} onChange={handleName} />
        <input value={username} onChange={handleUsername} />
      </div>
      <div>
        <div>
          <span>{name}</span>
        </div>
        <div>
          <span>{username}</span>
        </div>
      </div>
    </div>
  );
};

function App() {
  const [shouldRender, setShouldRender] = useState(true);

  useEffect(() => {
    setTimeout(() => {
      setShouldRender(false);
    }, 5000);
  }, []);

  return shouldRender ? <ForExample /> : null;
}

ReactDOM.render(<App />, document.getElementById("root"));

<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>

Solution 3 - Reactjs

To add to the accepted answer, I had a similar issue and solved it using a similar approach with the contrived example below. In this case I needed to log some parameters on componentWillUnmount and as described in the original question I didn't want it to log every time the params changed.

const componentWillUnmount = useRef(false)

// This is componentWillUnmount
useEffect(() => {
    return () => {
        componentWillUnmount.current = true
    }
}, [])

useEffect(() => {
    return () => {
        // This line only evaluates to true after the componentWillUnmount happens 
        if (componentWillUnmount.current) {
            console.log(params)
        }
    }

}, [params]) // This dependency guarantees that when the componentWillUnmount fires it will log the latest params

Solution 4 - Reactjs

Using custom js events you can emulate unmounting a componentWillUnmount even when having dependency.

Problem:

    useEffect(() => {
    //Dependent Code
    return () => {
        // Desired to perform action on unmount only 'componentWillUnmount' 
        // But it does not
        if(somethingChanged){
            // Perform an Action only if something changed
        }
    }
},[somethingChanged]);

Solution:

// Rewrite this code  to arrange emulate this behaviour

// Decoupling using events
useEffect( () => {
    return () => {
        // Executed only when component unmounts,
        let e = new Event("componentUnmount");
        document.dispatchEvent(e);
    }
}, []);

useEffect( () => {
    function doOnUnmount(){
        if(somethingChanged){
            // Perform an Action only if something changed
        }
    }

    document.addEventListener("componentUnmount",doOnUnmount);
    return () => {
        // This is done whenever value of somethingChanged changes
        document.removeEventListener("componentUnmount",doOnUnmount);
    }

}, [somethingChanged])

Caveats: useEffects have to be in order, useEffect with no dependency have to be written before, this is to avoid the event being called after its removed.

Solution 5 - Reactjs

function LegoComponent() {

  const [lego, setLegos] = React.useState([])
  
  React.useEffect(() => {
    let isSubscribed = true
    fetchLegos().then( legos=> {
      if (isSubscribed) {
        setLegos(legos)
      }
    })
    return () => isSubscribed = false
  }, []);

  return (
    <ul>
    {legos.map(lego=> <li>{lego}</li>)}
    </ul>
  )
}
 

In the code above, the fetchLegos function returns a promise. We can “cancel” the promise by having a conditional in the scope of useEffect, preventing the app from setting state after the component has unmounted.

> 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.

Solution 6 - Reactjs

instead of creating too many complicated functions and methods what I do is I create an event listener and automatically have mount and unmount done for me without having to worry about doing it manually. Here is an example.

//componentDidMount
useEffect( () => {

    window.addEventListener("load",  pageLoad);

    //component will unmount
    return () => {
       
        window.removeEventListener("load", pageLoad);
    }

 });

now that this part is done I just run anything I want from the pageLoad function like this.

const pageLoad = () =>{
console.log(I was mounted and unmounted automatically :D)}

Solution 7 - Reactjs

useEffect are isolated within its own scope and gets rendered accordingly. Image from https://reactjs.org/docs/hooks-custom.html

enter image description here

Solution 8 - Reactjs

Here is my solution, generalized into a custom hook:

import React, { useEffect, useRef } from 'react';

const useUnmountEffect = (effect, dependencies) => {
  if (typeof effect !== 'function') {
    console.error('Effect must be a function');
  }

  const componentWillUnmount = useRef(false)

  useEffect(() => () => {
    componentWillUnmount.current = true
  }, []);

  useEffect(() => () => {
    if (componentWillUnmount.current) {
      effect?.();
    }
  }, dependencies);
}

export default useUnmountEffect;

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionkooView Question on Stackoverflow
Solution 1 - ReactjsMehran MotieeView Answer on Stackoverflow
Solution 2 - ReactjsTholleView Answer on Stackoverflow
Solution 3 - ReactjsBarryView Answer on Stackoverflow
Solution 4 - ReactjsSS7View Answer on Stackoverflow
Solution 5 - Reactjstek fooView Answer on Stackoverflow
Solution 6 - ReactjsjerryurenaaView Answer on Stackoverflow
Solution 7 - ReactjsPeter Anthony DuotView Answer on Stackoverflow
Solution 8 - ReactjsBrad StiffView Answer on Stackoverflow