score:5

Accepted answer

why it looks like it doesn't work?

there are a couple hints that can help understand what's going on.

count is const, so it'll never change in its scope. it's confusing because it looks like it's changing when calling setcount, but it never changes, the component is just called again and a new count variable is created.

when count is used in a callback, the closure captures the variable and count stays available even though the component function is finished executing. again, it's confusing with useeffect because it looks like the callbacks are created each render cycle, capturing the latest count value, but that's not what's happening.

for clarity, let's add a suffix to variables each time they're created and see what's happening.

at mount time

function counter() {
  const [count_0, setcount_0] = usestate(0);

  useeffect(
    // this is defined and will be called after the component is mounted.
    () => {
      const id_0 = setinterval(() => {
        setcount_0(count_0 + 1);
      }, 1000);
      return () => clearinterval(id_0);
    }, 
  []);

  return <h1>{count_0}</h1>;
}

after one second

function counter() {
  const [count_1, setcount_1] = usestate(0);

  useeffect(
    // completely ignored by useeffect since it's a mount 
    // effect, not an update.
    () => {
      const id_0 = setinterval(() => {
        // setinterval still has the old callback in 
        // memory, so it's like it was still using
        // count_0 even though we've created new variables and callbacks.
        setcount_0(count_0 + 1);
      }, 1000);
      return () => clearinterval(id_0);
    }, 
  []);

  return <h1>{count_0}</h1>;
}

why does it work with let c?

let makes it possible to reassign to c, which means that when it is captured by our useeffect and setinterval closures, it can still be used as if it existed, but it is still the first one defined.

at mount time

function counter() {
  const [count_0, setcount_0] = usestate(0);

  let c_0 = count_0;

  // c_0 is captured once here
  useeffect(
    // defined each render, only the first callback 
    // defined is kept and called once.
    () => {
      const id_0 = setinterval(
        // defined once, called each second.
        () => setcount_0(c_0++), 
        1000
      );
      return () => clearinterval(id_0);
    }, 
    []
  );

  return <h1>{count_0}</h1>;
}

after one second

function counter() {
  const [count_1, setcount_1] = usestate(0);

  let c_1 = count_1;
  // even if c_1 was used in the new callback passed 
  // to useeffect, the whole callback is ignored.
  useeffect(
    // defined again, but ignored completely by useeffect.
    // in memory, this is the callback that useeffect has:
    () => {
      const id_0 = setinterval(
        // in memory, c_0 is still used and reassign a new value.
        () => setcount_0(c_0++),
        1000
      );
      return () => clearinterval(id_0);
    }, 
    []
  );

  return <h1>{count_1}</h1>;
}

best practice with hooks

since it's easy to get confused with all the callbacks and timing, and to avoid any unexpected side-effects, it's best to use the functional updater state setter argument.

// ❌ avoid using the captured count.
setcount(count + 1)

// ✅ use the latest state with the updater function.
setcount(currcount => currcount + 1)

in the code:

function counter() {
  const [count, setcount] = usestate(0);

  useeffect(() => {
    // i chose a different name to make it clear that we're 
    // not using the `count` variable.
    const id = setinterval(() => setcount(currcount => currcount + 1), 1000);
    return () => clearinterval(id);
  }, []);

  return <h1>{count}</h1>;
}

there's a lot more going on, and a lot more explanation of the language needed to best explain exactly how it works and why it works like this, though i kept it focused on your examples to keep it simple.


Related Query

More Query from same tag