componentWillUnmount with React useEffect hook
Asked Answered
B

4

112

How can the useEffect hook (or any other hook for that matter) be used to replicate componentWillUnmount?

In a traditional class component I would do something like this:

class Effect extends React.PureComponent {
    componentDidMount() { console.log("MOUNT", this.props); }
    componentWillUnmount() { console.log("UNMOUNT", this.props); }
    render() { return null; }
}

With the useEffect hook:

function Effect(props) {
  React.useEffect(() => {
    console.log("MOUNT", props);

    return () => console.log("UNMOUNT", props)
  }, []);

  return null;
}

(Full example: https://codesandbox.io/s/2oo7zqzx1n)

This does not work, since the "cleanup" function returned in useEffect captures the props as they were during mount and not state of the props during unmount.

How could I get the latest version of the props in useEffect clean up without running the function body (or cleanup) on every prop change?

A similar question does not address the part of having access to the latest props.

The react docs state:

If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run.

In this case however I depend on the props... but only for the cleanup part...

Bologna answered 13/3, 2019 at 10:17 Comment(2)
What is it that you want to do when the component is unmounted? There might be some other way of going about it.Tellurite
This is more a general question, since I wonder if there are cases which are not possible to replicate with hooks yet. But a concrete example would be storing the props into localStorage, IndexDB, or similar on unmount and reading them back on mount. I do not think this should be done on every prop change (e.g. keypress (if we render input fields)Bologna
V
79

You can make use of useRef and store the props to be used within a closure such as render useEffect return callback method

function Home(props) {
  const val = React.useRef();
  React.useEffect(
    () => {
      val.current = props;
    },
    [props]
  );
  React.useEffect(() => {
    return () => {
      console.log(props, val.current);
    };
  }, []);
  return <div>Home</div>;
}

DEMO

However a better way is to pass on the second argument to useEffect so that the cleanup and initialisation happens on any change of desired props

React.useEffect(() => {
  return () => {
    console.log(props.current);
  };
}, [props.current]);
Vendace answered 13/3, 2019 at 10:33 Comment(4)
Thank you very much! Using useRef works. I also applied your technique to the initial example (referenced in the question) here: codesandbox.io/s/48w5m9pkwx for reference purposes.Bologna
While this is the correct answer... it is still extremely unergonomic, especially when you're also depending on local state. Using a class component is sometimes the better way to go.Tweeddale
I'd refactor this solution into a custom hook. I'm calling mine useStateEffect, and the main difference is that the dependencies are passed to the effect function as arguments, which is how you access the current values.Unclassical
Here is a link to the appropriate documentation as well: reactjs.org/docs/hooks-effect.html#effects-with-cleanup. From the docs: "If your effect returns a function, React will run it when it is time to clean up:"Terminus
S
23

useLayoutEffect() is your answer in 2021

useLayoutEffect(() => {
    return () => {
        // Your code here.
    }
}, [])

This is equivalent to ComponentWillUnmount.

99% of the time you want to use useEffect, but if you want to perform any actions before unmounting the DOM then you can use the code I provided.

Spann answered 1/4, 2021 at 12:8 Comment(3)
I am sorry, but this does not solve the problem I raised. I need access to the props at point of unmounting and useLayoutEffect behaves no different than useEffect with the regards of running only on dependency changes (the empty array you passed). Therefor your solution only has access to the initial props of the FunctionComponent and not the "last props" during unmount.Bologna
Ahh sorry! I was up until 6 that morning, so I did not read thoroughly haha.Spann
I read the top two questions and answered on that. Yeah you have to put the variable you want to keep focus on within useRef for direct access to the newest changes. useEffect() return function actually runs after re-render (making the DOM inaccesible) while useLayoutEffect() return function runs before re-render (making the DOM accesible). This was the cleanest solution I had found and I had thought it was similar to yours. Perhaps this will be of some help anyway in the future :)Spann
B
2

useLayoutEffect is great for cleaning eventListeners on DOM nodes.

Otherwise, with regular useEffect ref.current will be null on time hook triggered

More on react docs https://reactjs.org/docs/hooks-reference.html#uselayouteffect

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

  const audioRef = useRef(null);


  useLayoutEffect(() => {
    if (!audioRef.current) return;

    const progressEvent = (e) => {
      setProgress(audioRef.current.currentTime);
    };

    audioRef.current.addEventListener('timeupdate', progressEvent);

    return () => {
      try {
        audioRef.current.removeEventListener('timeupdate', progressEvent);
      } catch (e) {
        console.warn('could not removeEventListener on timeupdate');
      }
    };
  }, [audioRef.current]);



Attach ref to component DOM node

<audio ref={audioRef} />

Bealle answered 10/2, 2022 at 14:28 Comment(0)
G
0
useEffect(() => {
  if (elements) {
    const cardNumberElement =
      elements.getElement('cardNumber') ||  // check if we already created an element
      elements.create('cardNumber', defaultInputStyles); // create if we did not
            
    cardNumberElement.mount('#numberInput');
  }
}, [elements]);
Garate answered 4/6, 2021 at 17:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.