reactjsreact-hooksuse-effect

useEffect simulating componentWillUnmount does not return updated state


I have a functional component that initializes a state with useState, then this state is changed via an input field.

I then have a useEffect hook simulating componentWillUnmount so that, before the component unmounts, the current, updated state is logged to the console. However, the initial state is logged instead of the current one.

Here is a simple representation of what I am trying to do (this is not my actual component):

import React, { useEffect, useState } from 'react';

const Input = () => {
    const [text, setText] = useState('aaa');

    useEffect(() => {
        return () => {
            console.log(text);
        }
    }, [])

    const onChange = (e) => {
        setText(e.target.value);
    };

    return (
        <div>
            <input type="text" value={text} onChange={onChange} />
        </div>
    )
}

export default Input;

I initialize the state as "initial." Then I use the input field to change the state, say I type in "new text." However, when the component in unmounted, "initial" is logged to the console instead of "new text."

Why does this happen? How can I access the current updated state on unmount?

Many thanks!

Edit:

Adding text to useEffect dependency array doesn’t solve my problem because in my real-world scenario, what I want to do is to fire an asynchronous action based on the current state, and it wouldn’t be efficient to do so everytime the “text” state changes.

I’m looking for a way to get the current state only right before the component unmounts.


Solution

  • You've effectively memoized the initial state value, so when the component unmounts that value is what the returned function has enclosed in its scope.

    Cleaning up an effect

    The clean-up function runs before the component is removed from the UI to prevent memory leaks. Additionally, if a component renders multiple times (as they typically do), the previous effect is cleaned up before executing the next effect. In our example, this means a new subscription is created on every update. To avoid firing an effect on every update, refer to the next section.

    In order to get the latest state when the cleanup function is called then you need to include text in the dependency array so the function is updated.

    Effect hook docs

    If you pass an empty array ([]), the props and state inside the effect will always have their initial values. While passing [] as the second argument is closer to the familiar componentDidMount and componentWillUnmount mental model, there are usually better solutions to avoid re-running effects too often.

    This means the returned "cleanup" function still only accesses the previous render cycle's state and props.

    EDIT

    useRef

    useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

    ...

    It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.

    Using a ref will allow you to cache the current text reference that can be accessed within the cleanup function.

    /EDIT

    Component

    import React, { useEffect, useRef, useState } from 'react';
    
    const Input = () => {
      const [text, setText] = useState('aaa');
    
      // #1 ref to cache current text value
      const textRef = useRef(null);
    
      // #2 cache current text value
      useEffect(() => {
        textRef.current = text;
      }, [text]);
    
      useEffect(() => {
        console.log("Mounted", text);
    
        // #3 access ref to get current text value in cleanup
        return () => console.log("Unmounted", text, "textRef", textRef.current);
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, []);
    
      useEffect(() => {
        console.log("current text", text);
        return () => {
          console.log("previous text", text);
        }
      }, [text])
    
      const onChange = (e) => {
        setText(e.target.value);
      };
    
      return (
        <div>
          <input type="text" value={text} onChange={onChange} />
        </div>
      )
    }
    
    export default Input;
    

    With the console.log in the returned cleanup function you'll notice upon each change in the input the previous state is logged to console.

    Edit zen-leftpad-twogm

    In this demo I've logged current state in the effect and previous state in the cleanup function. Note that the cleanup function logs first before the current log of the next render cycle.