React setInterval Behavior

て烟熏妆下的殇ゞ 提交于 2021-02-07 07:55:11

问题


let updateTimer: number;

export function Timer() {
  const [count, setCount] = React.useState<number>(0);
  const [messages, setMessages] = React.useState<string[]>([]);

  const start = () => {
    updateTimer = setInterval(() => {
      const m = [...messages];
      m.push("called");
      setMessages(m);
      setCount(count + 1);
    }, 1000);
  };

  const stop = () => {
    clearInterval(updateTimer);
  };

  return (
    <>
      <div>{count}</div>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
      {messages.map((message, i) => (
        <p key={i}>{message}</p>
      ))}
    </>
  );
}

Code Sample: https://codesandbox.io/s/romantic-wing-9yxw8?file=/src/App.tsx


The code has two buttons - Start and Stop.

  • Start calls a setInterval and saves interval id. Timer set to 1 second (1000 ms).

  • Stop calls a clearInterval on the interval id.

The interval id is declared outside the component.

The interval callback function increments a counter and appends a called message to the UI.

When I click on Start, I expect the counter to increment every second and a corresponding called message appended underneath the buttons.

What actually happens is that on clicking Start, the counter is incremented just once, and so is the called message.

If I click on Start again, the counter is incremented and subsequently reset back to its previous value.

If I keep clicking on Start, the counter keeps incrementing and resetting back to its previous value.

Can anyone explain this behavior?


回答1:


You have closure on count value inside the interval's callback.

Therefore after the first state update with value setState(0+1), you will have the same count value call setState(0+1) that won't trigger another render.

Use functional updates which uses the previous state value without closures:

setCount((count) => count + 1);

Same reason for messages:

setMessages(prev => [...prev,"called"]);
const start = () => {
  // should be a ref
  intervalId.current = setInterval(() => {
    setMessages((prev) => [...prev, "called"]);
    setCount((count) => count + 1);
  }, 1000);
};


Notice for another possible bug using an outer scope variable instead of useRef, for this read about useRef vs variable differences.


For a reference, here is a simple counter toggle example:

function Component() {
  // use ref for consisent across multiple components
  // see https://stackoverflow.com/questions/57444154/why-need-useref-to-contain-mutable-variable-but-not-define-variable-outside-the/57444430#57444430
  const intervalRef = useRef();

  const [counter, setCounter] = useState(0);

  // simple toggle with reducer
  const [isCounterOn, toggleCounter] = useReducer((p) => !p, false);

  // handle toggle
  useEffect(() => {
    if (isCounterOn) {
      intervalRef.current = setInterval(() => {
        setCounter((prev) => prev + 1);
      }, 1000);
    } else {
      clearInterval(intervalRef.current);
    }
  }, [isCounterOn]);

  // handle unmount
  useEffect(() => {
    // move ref value into callback scope
    // to not lose its value upon unmounting
    const intervalId = intervalRef.current;
    return () => {
      // using clearInterval(intervalRef.current) may lead to error/warning
      clearInterval(intervalId);
    };
  }, []);

  return (
    <>
      {counter}
      <button onClick={toggleCounter}>Toggle</button>
    </>
  );
}



来源:https://stackoverflow.com/questions/65489257/react-setinterval-behavior

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!