react hooks useEffect() cleanup for only componentWillUnmount?
Asked Answered
H

12

203

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)

Hyden answered 6/3, 2019 at 9:49 Comment(9)
You could have 2 separate effects. One which is given an array with username in it as second argument, and one that is given an empty array as second argument.Amnesty
@Amnesty Do you mean I have to make 2 seperate useEffect() methods?Hyden
Yes, that's one way to go about it.Amnesty
@Amnesty I thought it would be overrided by the last useEffect() method. I'll try. ThanksHyden
@Amnesty It works. thanks again. By the way, is there any prettier way to implement this? It feels like we write same-named methods twice.Hyden
Great! You're welcome. It depends on what the cleanup should do. 2 separate effects is not a bad solution.Amnesty
@Amnesty for me not works, i have work with redux, and i have a method which do flush all dat when component is unloaded, but not working for me, Help......Discomposure
@Racal Post your question, elaborate your issue with some codes, call me out with the tag, and maybe I can help you.Hyden
@Hyden i resolved this issue, but we can make friend on github. Thank you.Discomposure
A
159

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>
Amnesty answered 6/3, 2019 at 10:18 Comment(1)
nice and clean example. I am wondering though. Can I trigger a use effect somehow on changed navigation, or will I have to move it up in the components tree? Because when just pasting your "cleaned up" useEffect I am not seeing this trigger.Turnstile
S
277

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"), [] );
Seventh answered 7/3, 2019 at 10:20 Comment(6)
what is the difference between first and last useEffects, first useEffect will be invoked on the willmount or didmount, last useEffect was returned call back function with empty array why? could you elaborate each useEffect use cases when and how we can use?Patriciate
@siluverukirankumar Return value (function) of a callback is what is being called on destroy (unmount event). That is why the last example is a HOC, returning function immediately. The second parameter is where React would look for changes to rerun this hook. When it is an empty array it would run just once.Kikelia
Thanks @Kikelia got it the last useEffect is returning callback not seen clearlyPatriciate
So, if you make a useEffect hook, and it returns a function .. then the code before the returned function runs as a componentDidMount ... and the code in the returned function gets called for componentWillUnmount? It's a little confusing, so making sure I understand right. useEffect(()=>{ // code to run on mount ... return()=> { //code to run dismount}}) Is that right?Grivation
I suggest reading overreacted.io/a-complete-guide-to-useeffect Thinking about hooks in lifecycles isn't really that sweetPringle
This is nice. But I believe your second and third useEffect should read "did update" rather than "will update".Counterman
A
159

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>
Amnesty answered 6/3, 2019 at 10:18 Comment(1)
nice and clean example. I am wondering though. Can I trigger a use effect somehow on changed navigation, or will I have to move it up in the components tree? Because when just pasting your "cleaned up" useEffect I am not seeing this trigger.Turnstile
I
35

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
Illbehaved answered 28/1, 2021 at 15:52 Comment(2)
Order of the useEffects does matter, if anyone else was wonderingFourfold
Yes, @sirclesam, they are run the first time in appearance orderYanirayank
D
14

You can simply write it as :

useEffect(() => {
    return () => {};
}, []);
Dido answered 3/9, 2022 at 17:28 Comment(1)
This is the right answer. See thisMetopic
C
4

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.

Citizen answered 28/6, 2021 at 15:0 Comment(1)
The most important part of this answer is the caveat that you have to use useEffect hooks in orderMcgannon
H
3
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.

Hiragana answered 3/4, 2020 at 9:15 Comment(0)
C
2

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)}
Cati answered 31/7, 2020 at 23:35 Comment(0)
E
2

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;
Etana answered 19/5, 2022 at 18:1 Comment(0)
V
1

what about:

function useOnUnmount(callback: () => void) {
    const onUnmount = useRef<(() => void) | null>(null);
    onUnmount.current = callback;

    useEffect(() => {
        return () => onUnmount.current?.();
    }, []);
}

useOnUnmount(() => {
    console.log("unmount", props);
});
Viperous answered 2/6, 2022 at 14:59 Comment(0)
S
0

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

enter image description here

Spenserian answered 10/5, 2020 at 2:10 Comment(1)
A link to a solution is welcome, but please ensure your answer is useful without it: add context around the link so your fellow users will have some idea what it is and why it’s there, then quote the most relevant part of the page you're linking to in case the target page is unavailable. Answers that are little more than a link may be deleted.Haemato
S
0

Using two useEffect cleanups as seen in other solutions is not guaranteed because React does not guarantee the cleanup order of sibling effects.

You could try something like this, which will fire on component unmount or window close if enabled, whichever comes first. The first parameter is the callback, which you can change during the component's lifetime. The 2nd parameter is whether to enable the window closing event listener, which may only be set initially to avoid a scenario where we might have to rapidly register and unregister the same event listener, which can fail.

Note, if you see this firing once on mount and again on unmount, it is probably because you are in development mode and have React.StrictMode turned on. At least as of React 18, this will mount your component, unmount it, and then mount it again.

import { useEffect, useState } from "react";

export default function useWillUnmount(callback, beforeUnload = false) {
  const [cache] = useState({beforeUnload});
  cache.call = callback;

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

    function unmount() {
      if (cancel) return;
      cancel = true;
      cache.call?.();
    }

    if (cache.beforeUnload) window.addEventListener("beforeunload", unmount);

    return () => {
      if (cache.beforeUnload)
        window.removeEventListener("beforeunload", unmount);
      unmount();
    }
  }, [cache]); // this makes the effect run only once per component lifetime
}
Sinus answered 14/4, 2023 at 3:10 Comment(0)
T
0

With react 18 components will be mounted twice to test the stability in dev mode.

The logic we write on mount and unMount should respect this paradigm.

useMount

import { useEffect, useRef } from "react";

function useMount(run: () => void) {
  const referencedRun = useRef(run);
  useEffect(() => {
    referencedRun.current();
  }, []);
}

export default useMount;

useUnMount

import React, {
  DependencyList,
  EffectCallback,
  useEffect,
  useRef
} from "react";
import useMount from "./useMount";

const useUnMount = (effect: EffectCallback, dependencies?: DependencyList) => {
  const unMounted = useRef(false);

  // with react 18 component will be mounted twice in dev mode, so setting the reference to false on the second mount
  useMount(() => {
    unMounted.current = false;
  });

  // to identify unmount
  useEffect(
    () => () => {
      unMounted.current = true;
    },
    []
  );

  // wrap dependencies with the callback
  useEffect(
    () => () => {
      if (unMounted.current) {
        effect();
      }
    },
    [dependencies, effect]
  );
};

export default useUnMount;

Usage

  const [val, setVal] = useState("");

  useMount(() => {
    console.log("on mount...", val);
  });

  useUnMount(() => {
    console.log("on unmount...", val);
  }, [val]);

Notice that on unmount we are passing the dependencies to resolve the latest value.

Trojan answered 1/9, 2023 at 13:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.