Note: How to simulate componentWillUnmount in Hooks?

  // passing the empty array([])
  useEffect(() => {
    //  simulate componentDidMount lifecycle
    return () => {
      // simulate componentWillUnmount lifecycle
    };
  }, []);

Problem: The props and state inside the effect will always have their initial values when passing an empty array([]).

Take a look at the example below.
The Demo Component can be shown or hidden, so we can make this component mount or unmount.

function App() {
  const [demoVisible, setDemoVisible] = useState(true);
  return (
     <div>
        <h2>Demo showcase</h2>
        <button
          onClick={() => {
            setDemoVisible((prev) => !prev);
          }}
        >
          toggle ComponentVisible
        </button>
        {demoVisible && <Demo />}
    </div>
  );
}

There is a increment button to change the count state.
Click the increment button several times and hide the Demo Component via clicking the toggle button to trigger the cleanup function in useEffect.
The count value is also the initial value 0.

function Demo() {
  const [count, setCount] = useState(0);

  const onClick = () => {
    setCount((prev) => prev + 1);
  };

  useEffect(() => {
    console.log(`count in effect: ${count}`); // initial value 0
    return () => {
      console.log(`count in effect cleaner: ${count}`); // initial value 0
    };
  }, []);

  return (
    <div>
      <button onClick={onClick}>increment</button>
      <div>count value: {count} </div>
    </div>
  );
}

Why? Because of the Closure. The callback function of useEffect refers to count variable through the Closure. It doesn't change no matter how many times re-render occurs. It is always the value of the first render.

Solution: How to get the latest state in useEffect when passing an empty array([]) ?

Solution 1: Add count to the useEffect dependency list.

function DependencySolution() {
  
  // ...
  useEffect(() => {
    console.log(`count in effect of DependencySolution: ${count}`);
    return () => {
      console.log(`count in effect cleaner of DependencySolution: ${count}`);
    };
  }, [count]);

  // ...
}

When we hide the component, we can the the latest count in useEffect cleanup callback.

But this way is not very friendly, if the callbacks in useEffect are about event. Like the below code.

   useEffect(() => {
        const onResize = () => {};
        window.addEventListener('resize', onResize);
        return(() => {
            window.removeEventListener('resize', onResize);
        });
    }, [count]);

This would hardly be efficient. It's a bad way to frequently add and remove the event listener.

Solution 2: useRef

We can use useRef to store the latest value of the state.
The useRef returned object will persist for the full lifetime of the component.
countRef doesn't change, what changes is its current properties

function RefSolution() {
  // ...
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  useEffect(() => {
    console.log(`count in effect of RefSolution: ${countRef.current}`);
    return () => {
      console.log(
        `count in effect cleaner of RefSolution: ${countRef.current}`
      );
    };
  }, []);
// ...
} 

In this example, we are using a state in Component, but the same problem and solutions could also be applied to props .

You can try the code on stackblitz code edit