26

The method componentWillUnmount() is invoked immediately before a component is unmounted and destroyed. If we use useEffect with an empty array ([]) as the second argument and put our function in return statement it will be executed after the component is unmounted and even after another component will be mounted. This is done for performance reasons as far as I understand. In order not to delay rendering.

So the question is - how can we call some function using hooks before a component gets unmounted?

What I am trying to do is an application which saves user's input as he types (without submitting form). I use setInterval to save updated text every N seconds. And I need to force save updates before the component will unmount. I don't want to use prompt by react router before navigating. This is an electron application. I appreciate any thoughts or advice on how to implement such functionality.

Update

Unfortunately, Effects with Cleanup run after letting the browser paint. More details can be found here: So What About Cleanup?. It basically means that cleanup is run after a component is unmounted and it is not the same as executing code in componentWillUnmount(). I can clearly see the sequence of calls if I put console.log statements in the cleanup code and in another component. The question is whether we can execute some code before a component is unmounted using hooks.

Update2

As I can see I should better describe my use case. Let's imagine a theoretical app which holds its data in a Redux store. And we have two components with some forms. For simplicity, we don't have any backend or any async logic. We use only Redux store as data storage.

We don't want to update Redux store on every keystroke. So we keep actual values in the local component's state which we initialize with values from the store when a component mounts. We also create an effect which sets up a setInterval for 1s.

We have the following process. A User types something. Updates are stored in the local component state until our setInterval callback is called. The callback just puts data in the store (dispatches action). We put our callback in the useEffect return statement to force save to store when the component gets unmounted because we want to save data to store in this case as soon as possible.

The problem comes when a user types something in the first component and immediately goes to the second component (faster than 1s). Since the cleanup in our first component will be called after re-rendering, our store won't be updated before the second component gets mounted. And because of that, the second component will get outdated values to its local state.

If we put our callback in componentWillUnmount() it will be called before unmounting and the store will be updated before the next component mounts. So can we implement this using hooks?

Georgy Nemtsov
  • 786
  • 1
  • 8
  • 19
  • Please read [Effects with Cleanup](https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup) . This has the answer. – Arup Rakshit Apr 21 '19 at 19:54
  • @ArupRakshit, I just reread that and it still does not answer the question. The return function on a hook runs AFTER unmount, how do you run code with hooks BEFORE unmount? – David Bradshaw May 13 '20 at 09:05

9 Answers9

28

componentWillUnmount can be simulated by returning a function inside the useEffect hook. The returned function will be called just before every rerendering of the component. Strictly speaking, this is the same thing but you should be able to simulate any behaviour you want using this.

useEffect(() => {
  const unsubscribe = api.createSubscription()
  return () => unsubscribe()
})

Update

The above will run every time there is a rerender. However, to simulate the behaviour only on mounting and unmounting (i.e. componentDidMount and componentWillUnmount). useEffect takes a second argument which needs to be an empty array.

useEffect(() => {
  const unsubscribe = api.createSubscription()
  return () => unsubscribe()
}, [])

See a more detailed explanation of the same question here.

thecartesianman
  • 745
  • 1
  • 6
  • 15
  • 8
    It seems that this is like most people think about it. But in reality, the returned function is not called before rerendering. It is called after rerendering. – Georgy Nemtsov Apr 22 '19 at 06:52
  • For me it is triggered on every component rendering (not on unmounting only). I use React 16.12.0. – Alexander Elgin Feb 19 '20 at 05:52
  • useEffect takes a second argument in that case, to simulate just unmounting pass in an empty array e.g. useEffect(() => {...}, [ ]) – thecartesianman Feb 24 '20 at 20:12
  • 2
    In case I use the empty array, what do I do if I want to read a state but it keeps complaining that I should add it to the dependency array? – sepha Feb 26 '20 at 09:05
  • 3
    Whilst this clearly solves some people issues, it does not answer the question. The return function on a hook runs AFTER unmount, how do you run code with hooks BEFORE unmount? – David Bradshaw May 13 '20 at 09:07
10

Since the introduction of the useLayoutEffect hook, you can now do

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

to simulate componentWillUnmount. This runs during unmount, but before the element has actually left the page.

Frank
  • 101
  • 1
  • 2
  • 1
    This solved a similar problem of mine too: I added an eventListener on one of my component's children in useEffect, but in the useEffect return callback that children was already unmounted and removed from the DOM -> I was not able anymore to RemoveEventListener on it. Using useLayoutEffect solved that problem, thank you very much. – MamorukunBE Apr 15 '22 at 21:57
6

The question here is how do you run code with hooks BEFORE unmount? The return function with hooks runs AFTER unmount and whilst that doesn’t make a difference for most use cases, their are some where it is a critical difference.

Having done a bit of investigation on this, I have come to the conclusion that currently hooks simply does not provide a direct alternative to componentWillUnmount. So if you have a use case that needs it, which is mainly for me at least, the integration of non-React libs, you just have to do it the old way and use a component.

Update: see the answer below about UseLayoutEffect() which looks like it may solve this issue.

David Bradshaw
  • 11,859
  • 3
  • 41
  • 70
  • 1
    Nope, `useLayoutEffect` doesn't really change anything here because the cleanup will still run after a new component is mounted. So the local state of this new component will still be initialized with the wrong value from the store. I think your original answer remains correct. – Georgy Nemtsov Nov 03 '21 at 08:30
5

I agree with Frank, but the code needs to look like this otherwise it will run only on the first render:

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

This is equivalent to ComponentWillUnmount

Simen L
  • 378
  • 3
  • 10
4

Similar to @pritam's answer, but with an abstracted code example. The whole idea of useRef is to allow you to keep track of the changes to the callback and not have a stale closure at the time of execution. Hence, the useEffect at the bottom can have an empty dependency array to ensure it only runs when the component unmounts. See the code demo.

Reusable hook:

type Noop = () => void;

const useComponentWillUnmount = (callback: Noop) => {
    const mem = useRef<Noop>();

    useEffect(() => {
        mem.current = callback;
    }, [callback]);

    useEffect(() => {
        return () => {
            const func = mem.current as Noop;
            func();
        };
    }, []);
};
jaimefps
  • 2,244
  • 8
  • 22
  • 45
1

After a bit of research, found that - you could still accomplish this. Bit tricky but should work.

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]);
pritam
  • 2,452
  • 1
  • 21
  • 31
1

I got in a unique situation where the useEffect(() => () => { ... }, []); answers did not work for me. This is because my component never got rendered — I was throwing an exception before I could register the useEffect hook.

function Component() {
  useEffect(() => () => { console.log("Cleanup!"); }, []);

  if (promise) throw promise;
  if (error) throw error;

  return <h1>Got value: {value}</h1>;
}

In the above example, by throwing a Promise<T> that tells react to suspend until the promise is resolved. However, once the promise is resolved, an error is thrown. Since the component never gets rendered and goes straight to an ErrorBoundary, the useEffect() hook is never registered!

If you're in a similar situation as myself, this little code may help:

To solve this, I modified my ErrorBoundary code to run a list of teardowns once it was recovered

export default class ErrorBoundary extends Component {
  // ...

  recover() {
    runTeardowns();
    // ...
  }

  // ...
}

Then, I created a useTeardown hook which would add teardowns that needed to be ran, or make use of useEffect if possible. You'll most likely need to modify it if you have nesting of error boundaries, but for my simple usecase, it worked wonderfully.

import React, { useEffect, useMemo } from "react";
const isDebugMode = import.meta.env.NODE_ENV === "development";

const teardowns: (() => void)[] = [];

export function runTeardowns() {
  const wiped = teardowns.splice(0, teardowns.length);

  for (const teardown of wiped) {
    teardown();
  }
}

type Teardown = { registered?: boolean; called?: boolean; pushed?: boolean } & (() => unknown);

/**
 * Guarantees a function to run on teardown, even when errors occur.
 *
 * This is necessary because `useEffect` only runs when the component doesn't throw an error.
 * If the component throws an error before anything renders, then `useEffect` won't register a
 * cleanup handler to run. This hook **guarantees** that a function is called when the component ends.
 *
 * This works by telling `ErrorBoundary` that we have a function we would like to call on teardown.
 * However, if we register a `useEffect` hook, then we don't tell `ErrorBoundary` that.
 */
export default function useTeardown(onTeardown: () => Teardown, deps: React.DependencyList) {
  // We have state we need to maintain about our teardown that we need to persist
  // to other layers of the application. To do that, we store state on the callback
  // itself - but to do that, we need to guarantee that the callback is stable. We
  // achieve this by memoizing the teardown function.
  const teardown = useMemo(onTeardown, deps);

  // Here, we register a `useEffect` hook to run. This will be the "happy path" for
  // our teardown function, as if the component renders, we can let React guarantee
  // us for the cleanup function to be ran.
  useEffect(() => {
    // If the effect gets called, that means we can rely on React to run our cleanup
    // handler.
    teardown.registered = true;

    return () => {
      if (isDebugMode) {
        // We want to ensure that this impossible state is never reached. When the
        // `runTeardowns` function is called, it should only be ran for teardowns
        // that have not been able to be hook into `useEffect`.
        if (teardown.called) throw new Error("teardown already called, but unregistering in useEffect");
      }

      teardown();

      if (isDebugMode) {
        // Because `teardown.registered` will already cover the case where the effect
        // handler is in charge of running the teardown, this isn't necessary. However,
        // this helps us prevent impossible states.
        teardown.called = true;
      }
    };
  }, deps);

  // Here, we register the "sad path". If there is an exception immediately thrown,
  // then the `useEffect` cleanup handler will never be ran.
  //
  // We rely on the behavior that our custom `ErrorBoundary` component will always
  // be rendered in the event of errors. Thus, we expect that component to call
  // `runTeardowns` whenever it deems it appropriate to run our teardowns.

  // Because `useTeardown` will get called multiple times, we want to ensure we only
  // register the teardown once.
  if (!teardown.pushed) {
    teardown.pushed = true;

    teardowns.push(() => {
      const useEffectWillCleanUpTeardown = teardown.registered;

      if (!useEffectWillCleanUpTeardown) {
        if (isDebugMode) {
          // If the useEffect handler was already called, there should be no way to
          // re-run this teardown. The only way this impossible state can be reached
          // is if a teardown is called multiple times, which should not happen during
          // normal execution.
          const teardownAlreadyCalled = teardown.called;
          if (teardownAlreadyCalled) throw new Error("teardown already called yet running it in runTeardowns");
        }

        teardown();

        if (isDebugMode) {
          // Notify that this teardown has been called - useful for ensuring that we
          // cannot reach any impossible states.
          teardown.called = true;
        }
      }
    });
  }
}
SirJosh3917
  • 309
  • 5
  • 13
0

It does not matter wether the returned function from useEffect gets called before or after the component unmounted: You still have access to the states valuey through the closure:

  const [input, setInput] = useState(() => Store.retrieveInput());

  useEffect(() => {
    return () => Store.storeInput(input); // < you can access "input" here, even if the component unmounted already
  }, []);

If you don't manage the input in the components state, your whole structure is broken and should be changed to manage state at the right place. In your case, you should lift the shared input state of the components to the parent.

Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • 1
    Thanks, but the question is not about how to access values. I updated my question with a more detailed scenario to make it clearer. I tend to think that hooks can't do that and in such case, we still need class components. – Georgy Nemtsov Apr 23 '19 at 07:07
  • 1
    @georgy can't you lift the state up to the parent? – Jonas Wilms Apr 23 '19 at 07:16
  • 1
    Yes, it is an option. But I would prefer using a class component with `componentWillUnmount()` instead. Lifting state up makes code harder to maintain in my opinion. – Georgy Nemtsov Apr 25 '19 at 08:31
0

ReactJS docs on hooks specify this:

Effects may also optionally specify how to “clean up” after them by returning a function.

So any function you return in your useEffect hook, will be executed when the component unmounts, as well as before re-running the effect due to a subsequent render.

pritam
  • 2,452
  • 1
  • 21
  • 31