How to prevent inline functions from binding to old state values

Viewed 255

In a project with React components using hooks, I am trying to understand how to properly avoid calling callbacks that are bound to old state values. The below example illustrates the issue (but is not the code I am working on).

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

const Message = () => {
  const [message, setMessage] = useState("");

  function doStuff() {
    console.log(message);
  }

  useEffect(() => {
    setInterval(doStuff, 1000)
  }, []);

  return (
    <div>
      <input
        type="text"
        value={message}
        placeholder="Enter a message"
        onChange={e => setMessage(e.target.value)}
      />
      <p>
        <strong>{message}</strong>
      </p>
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<Message />, rootElement);

The problem here is of course that setInterval will keep the doStuff function as it was when the effect was called the first (and only time). And at that time the message state was empty and hence, the interval function will print an empty string every second instead of the message that is actually inside the text box.

In my real code, I am having external events that should trigger function calls inside the component, and they suffer this same issue.

What should I do?

2 Answers

You should useCallback and pass it as a dependency to your effect.

const doStuff = useCallback(() => {
  console.log(message);
}, [message]);

useEffect(() => {
  const interval = setInterval(doStuff, 1000);
  return () => clearInterval(interval); // clean up
}, [doStuff]);

Here when message gets updated it will have its new value in the doStuff

You can put this in it's own hook also. I have this in my production code

/**
 * A react hook for setting up an interval
 * @param handler - Function to execute on interval
 * @param interval - interval in milliseconds
 * @param runImmediate - If the function is executed immediately
 */
export const useInterval = (handler: THandlerFn, interval: number | null, runImmediate = false): void => {
  const callbackFn = useRef<THandlerFn>()

  // Update callback function
  useEffect((): void => {
    callbackFn.current = handler
  }, [handler])

  // Setup interval
  useEffect((): (() => void) | void => {
    const tick = (): void => {
      callbackFn.current && callbackFn.current()
    }

    let timerId: number
    if (interval) {
      if (runImmediate) {
        setTimeout(tick, 0)
      }

      timerId = setInterval(tick, interval)

      return (): void => {
        clearInterval(timerId)
      }
    }
  }, [interval, runImmediate])
}
Related