Reputation: 16586
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
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:
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.
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
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