temporary_user_name
temporary_user_name

Reputation: 37108

How to update the unmount handler in a useEffect without firing it repeatedly?

Another developer came to me with a tricky question today. She proposed the following:

A.) She wants to fire an event whenever a modal closes.
B.) She does not want the event to fire at any other time.
C.) The event must be up to date with state of the component.

A basic example is like so:

const ModalComponent = () => {
    const [ eventData, setEventData ] = useState({ /* default data */ });

    useEffect(() => {
        return () => {
            // happens anytime dependency array changes
            eventFire(eventdata);
        };
    }, [eventData]);
   
    return (
       <>
         // component details omitted, setEventData used in here
       </>
    );
};

The intention is that when the modal is closed, the event fires. However, user interactions with the modal cause state updates that change the value of eventData. This leads to the core of the problem:

  1. Leaving eventData out of the useEffect dependency array causes it to fire only at time of modal closing, but the value is stale. It's the value that it was at the time the component mounted.
  2. Placing eventData in the useEffect dependency array causes the event to fire over and over, whenever the data changes and the component re-renders and updates useEffect.

What is the solution to this? How can you access up-to-date data yet only act on it at time of unmounting?

Upvotes: 5

Views: 1457

Answers (1)

Diego Fidalgo
Diego Fidalgo

Reputation: 475

Store eventData in a ref.

const [eventData, setEventData] = useState({ /* default data */ });
const ref = useRef();

ref.current = eventData;

useEffect(() => () => eventFire(ref.current), []);

This will keep the value always up to date since it won't be locked into the function closure and will remove the need for triggering the effect every time eventData changes.

Also, you can extract this logic into a custom hook.

function useStateMonitor(state) {
  const ref = useRef();
  ref.current = state;
  return () => ref.current;
}

And usage would be like this

const [eventData, setEventData] = useState({ /* default data */ });
const eventDataMonitor = useStateMonitor(eventData);

useEffect(() => () => eventFire(eventDataMonitor()), []);

Here's a working example

Upvotes: 5

Related Questions