Home > Software design >  React setInterval is re-initialized to often
React setInterval is re-initialized to often

Time:07-21

I've a hard time understanding how setInterval works. My main problem is that an interval is re-initialized too often. Basically, I want a context-sensitive sidebar to be modfied by MainElement, and I want this sidebar to do something at a regular base. In the real scenario there the timer gets cancelled when unmounting ofc.

import { useEffect, useState } from 'react';

// This is the component called from outside
export const MainLayout = () => {
  const [element2Content, setElement2Content] = useState<string | null>(null);
  return (
    <>
      <MainElement setElement2Content={setElement2Content}>
        Element1
      </MainElement>
      {element2Content && <Sidebar content={element2Content} />}
    </>
  );
};

// This component manipulates the sidebar via useEffect
const MainElement: React.FC<{ setElement2Content: (input: string) => void }> =
  ({ setElement2Content, children }) => {
    useEffect(() => setElement2Content('content set from element 1'), []);
    return <div>{children}</div>;
  };

// This component utilizes the setInterval, but doesn't work as expected
const Sidebar: React.FC<{ content: string }> = ({ content }) => {
  const [calls, setCalls] = useState(0);
  useEffect(() => {
    setInterval(() => {
      console.log('interval called for', calls   1, 'times');
      setCalls(calls   1);
    }, 1000);
  }, []);
  return <div>{`content${content}, calls: ${calls}`}</div>;
};

The log is just interval called for 1 times in a loop. In the browser I see the components rendered, and I see interval called for 0 times being changed to interval called for 1 times, where it stops.

So I'm wondering: Why does it stop at 1? It seems like setInterval gets reset all the time.

To understand the behavior of a timer a bit more, I changed my MainElement to

const MainElement: React.FC<{ setElement2Content: (input: string) => void }> =
  ({ setElement2Content, children }) => {
    useEffect(() => setElement2Content('content set in element 1'), []);
    useEffect(() => {
      setInterval(() => {
        console.log('interval called from mainelement');
      }, 1000);
    }, []);
    return <div>{children}</div>;
  };

Now, for some reason the MainElement is also re-rendered repeatedly, and so is the sidebar. The console logs

interval called in mainelement
interval called for 1 times

I'ld be grateful for any ideas or explanations!

CodePudding user response:

The interval is running correctly, but in your interval function calls is never being updated passed 1. This is because the calls variable in the setInterval function is stale (i.e. it is always equal to 0 when the function is invoked, so setCalls(call 1) will always update calls to 1.) This staleness is because the calls variable in the function is captured in the closure formed when the effect is run during the first render of the component and never updated thereafter.

A quick way to address this is to use the functional version of the setState call:

  useEffect(() => {
    setInterval(() => {
      console.log('interval called for', calls   1, 'times');
      setCalls(prevCalls => prevCalls   1);
    }, 1000);
  }, []);
  • Related