Nick
Nick

Reputation: 16586

Why does Solid.js createEffect not re-run when a signal is in a setTimeout callback?

In Solid, why does this effect not re-run when count is updated? After some tinkering, I've found that it has to with count being in the setTimeout callback function, but what's the intuitive way to understand what things inside an effect are tracked and what things aren't?

function Counter() {
  const [count, setCount] = createSignal(0);

  createEffect(() => {
    setTimeout(() => {
      setCount(count() + 1);
    }, 1000);
  })

  return (
    <>
      {count()}
    </>
  );
}

Upvotes: 7

Views: 3054

Answers (2)

snnsnn
snnsnn

Reputation: 13698

That is because the effect can not re-subscribe to the signal once it is run. Here is why:

Solid runs synchronously. Every signal keeps its own subscribers list. Effects are added to the subscribers list when they read the signal and removed when they are called back. So, subscribers list is renewed in each update cycle and it happens synchronously.

However setTimeout's callback is run asynchronously in the event loop. When the callback runs, it will update the signal's value and the effect wrapping the setTimeout function will be added to the subscribers list. However this subscribers list gets discarded when the signal completes its execution cycle. So, the effect will never be called back. In other words, the effect will be subscribing to the subscribers list of the previous execution cycle.

The problem is not that the callback is unable to update the signal, (actually it does, that is why counter increments by one), but the effect is unable to re-subscribe to the signal's queue. So, we need to find a way to make the effect re-subscribe to the signal.

You have two options which produce different outputs:

  1. Reads the signal synchronously which makes the effect re-subscribe and set a new timer whenever signal updates:
import { render } from "solid-js/web";
import { createSignal, createEffect } from "solid-js";

function Counter() {
  const [count, setCount] = createSignal(0);

  createEffect(() => {
    const c = count();
    setTimeout(() => {
      setCount(c + 1);
    }, 1000);
  })

  return (
    <>
      {count()}
    </>
  );
}

We read the signal's value in advance, long before the setTimeout's callback gets fired.

  1. Get the right owner and subscribe to its list:
function Counter() {
  const [count, setCount] = createSignal(0);

  const owner = getOwner();
  
  setTimeout(() => {
    runWithOwner(owner!, () => {
      createEffect(() => {
        console.log('Running Effect');
        setCount(count() + 1);
      });
    });
  }, 1000);

  return (
    <>
      {count()}
    </>
  );
}

In this solution, we create the effect when the setTimeout's callback is fired and bind the effect to the current owner.

An important side note: Your code will cause an infinite loop because you are setting the signal inside the effect, which runs whenever signal updates.

createEffect(() => {
  setCount(count() + 1);
});

You can read more about runWithOwner function here: https://www.solidjs.com/docs/latest/api#runwithowner

Upvotes: 1

Nick
Nick

Reputation: 16586

You can think about it this way (this is pretty much how the source code works):

let Listener

function Counter() {
  const [count, setCount] = createSignal(0);

  createEffect(() => {
    Listener = thisEffect
    setTimeout(() => {
      setCount(count() + 1);
    }, 1000);
    Listener = null
  })

  return (
    <>
      {count()}
    </>
  );
}

As you can see the effect will set itself as the listener (tracking context) when the function starts and then will reset the listener (to the previous listener if it exists, in this case it doesn't).

So the effect will be the tracking context only during the execution of the callback you provided to createEffect as the argument. setTimeout delays the execution of whatever you put in it, so once the callback you put in setTimeout executes, the effect callback will have already finished executing, which means that it has already reset the listener, so the effect is not listening to signals anymore.

Upvotes: 7

Related Questions