108

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

DAG
  • 6,710
  • 4
  • 39
  • 63
  • What is it that you want to do when the component is unmounted? There might be some other way of going about it. – Tholle Mar 13 '19 at 10:28
  • 6
    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) – DAG Mar 13 '19 at 10:31

4 Answers4

76

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]);
Tholle
  • 108,070
  • 19
  • 198
  • 189
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • 1
    Thank you very much! Using `useRef` works. I also applied your technique to the initial example (referenced in the question) here: https://codesandbox.io/s/48w5m9pkwx for reference purposes. – DAG Mar 13 '19 at 10:39
  • 25
    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. – hasufell Nov 21 '19 at 08:20
  • 3
    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. – Tom Jul 30 '20 at 19:06
  • Here is a link to the appropriate documentation as well: https://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:" – ethaning Oct 19 '20 at 17:43
21

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.

Simen L
  • 378
  • 3
  • 10
  • 4
    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. – DAG Apr 01 '21 at 17:35
  • Ahh sorry! I was up until 6 that morning, so I did not read thoroughly haha. – Simen L Apr 02 '21 at 08:27
  • 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 :) – Simen L Apr 02 '21 at 08:38
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} />

Sergey Khmelevskoy
  • 2,429
  • 3
  • 19
  • 43
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]);
Michael Rovinsky
  • 6,807
  • 7
  • 15
  • 30
Andrii Svirskyi
  • 376
  • 1
  • 4