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

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()}
</>
);
}

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.

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

Related

clearInterval and eventListener

My problem is, I'm not being able to use the 'setInterval' attached to a button and use another one to stop it with 'clearInterval'.
My thought process:
Since clearInterval() would need a target, I stored the setInterval() inside a variable. However, I noticed that setInterval() starts running without being triggered by the eventListener attached to it. I was expecting this behaviour of firing on its own only if I had put setInterval() out in the open without being stored on a variable and on the global scope.
So then I tried to encapsulate it inside a function, only this time the clearInterval() seemed to 'lose' the target.
I had a look on some situtations and couldn't find an explanation that would satisfy my case. Right now it looks like I can't have both ways (a btn to 'fire' the timer and a btn to shut it off).
I don't know if this is a matter of scope,event handling or targeting. Had a look on HTMLStandard but couldn't make much use of it.
Thank you
<script>
let repeater = function () {
setInterval(() => {
console.log('loading');
}, 1000);
};
const startButton = document.querySelector('#firstB');
const stopButton = document.querySelector('#secondB');
startButton.addEventListener('click', repeater);
stopButton.addEventListener('click', () => {
clearInterval(repeater);
console.log('finished');
});
</script>
const startButton = document.querySelector("#firstB");
const stopButton = document.querySelector("#secondB");
let repeater;
startButton.addEventListener("click", function () {
repeater = setInterval(() => {
console.log("loading");
}, 1000);
});
stopButton.addEventListener("click", () => {
clearInterval(repeater);
console.log("finished");
});
This should work.

res.data won't save to state correctly

I have a MERN app which pulls data from a collection in MongoDB to render a timer component in the DOM. Currently in my collection, I have three timers titled first timer, another timer and even another. When I make a get request and run console.log(res.data), I see all the timers and their relevant data logged to the console. However, when I try to set state of timers using the useState hook, only the last timer is saved to state. Here is the code of my component:
function Wrapper() {
const [timers, setTimers] = useState([]);
const [title, setTitle] = useState('');
useEffect(() => {
axios
.get('http://localhost:3001/')
.then((res) => {
console.log(res.data);
res.data.map((timer) => {
let newTimer = (
<Timer title={timer.title} id={timer._id} time={timer.time} />
);
let allTimers = timers.slice();
allTimers.push(newTimer);
setTimers(allTimers);
console.log(allTimers);
});
})
.catch((err) => {
console.log(err);
});
}, []);
Here I am making my get request and mapping through the res.data to create a new timer component for each iteration. Then, I make a copy of timers (since state is immutable), push my new timer to allTimers variable and finally run setTimers(allTimers). Here is what React renders:
I expect allTimers to contain one, two and then all three of the timers in my database when logged to the consol on line 17. However, only the most recent timer shows up so it seems like I am setting state incorrectly but I'm not sure how. Anyone have any suggestions?
Because you set useEffect to Only runs on initial render, it always refers to the initial state of timers, which is an empty array, and even as you try to update it with useState but on the next loop it still refers to the empty array. The only update to have real effect is the last one, pushing the third timer into the empty array.
You can move your entire map loop to the return statement of the functional component to render an element for each timer.

How exactly does useEffect's return work? Why is the code preforming like it does?

I'm practicing react hooks and I'm creating a very simple stopwatch app. Currently, my code is doing exactly what I want it to do but I do not understand why it works. When I hit start, the setTimeouts run and constantly update the time state. When I hit stop, it clears the timeout. Why does it clear the timeout when I do not explicitly tell it to. Also, based on the react docs, the return in useEffect will only run when the component unmounts. However, I threw console.logs inside and saw that it runs the returned callback every time useEffect is called. Finally, I removed the returned callback and saw that it doesn't actually clear the timeout when I hit stop. Can someone help me dissect this?
import React, {useState, useEffect} from 'react';
function Stopwatch(){
const [time, setTime] = useState(0);
const [start, setStart] = useState(false);
useEffect(() => {
let timeout;
if (start) {
timeout = setTimeout(() => {setTime(currTime => currTime + 1);}, 1000);
}
return () => {
clearTimeout(timeout);
}
});
return(
<>
<div>{time}</div>
<button onClick={() => setStart(currStart => !currStart)}>{start ? "Stop" : "Start"}</button>
</>
)
}
export default Stopwatch
Why does it clear the timeout when I do not explicitly tell it to?
In your implementation useEffect runs after every re-render because you didn't specify the dependencies array, so if you start the timer and then in the middle press stop the clean up function is going to run and the last timeout will be cleared
It goes like this,
The component mounts -> useEffect callback fires and returns a function -> when the component re-renders, the returned function is executed and the cycle goes back to running the useEffect callback.
What you probably read in the docs had an empty dependencies array which is the second argument of useEffect
useEffect(() => {
console.log('will only run when the component mounts for the first time')
return () => {
console.log('will only run when the component unmounts')
}
}, []) // nothing inside the dependencies array, run this once
A better implementation of your component will be like this
function Stopwatch(){
const [time, setTime] = useState(0)
const [start, setStart] = useState(false)
useEffect(() => {
// when start is false there is no reason to set up a timer or return a
// cleanup function so lets just exit early
if (!start) return
// start is true, set up the interval
const intervalId = setInterval(() => setTime(prevTime => prevTime + 1), 1000)
// return a cleanup function that will run only when start changes
// to false
return () => clearInterval(intervalId)
}, [start]) // run this effect only when start changes
const toggleStart = () => setStart(prevStart => !prevStart)
return(
<>
<div>{time}</div>
<button onClick={toggleStart}>{start ? "Stop" : "Start"}</button>
</>
)
}

RxJs Observable with infinite scroll OR how to combine Observables

I have a table which uses infinite scroll to load more results and append them, when the user reaches the bottom of the page.
At the moment I have the following code:
var currentPage = 0;
var tableContent = Rx.Observable.empty();
function getHTTPDataPageObservable(pageNumber) {
return Rx.Observable.fromPromise($http(...));
}
function init() {
reset();
}
function reset() {
currentPage = 0;
tableContent = Rx.Observable.empty();
appendNextPage();
}
function appendNextPage() {
if(currentPage == 0) {
tableContent = getHTTPDataPageObservable(++currentPage)
.map(function(page) { return page.content; });
} else {
tableContent = tableContent.combineLatest(
getHTTPDataPageObservable(++currentPage)
.map(function(page) { return page.content; }),
function(o1, o2) {
return o1.concat(o2);
}
)
}
}
There's one major problem:
Everytime appendNextPage is called, I get a completely new Observable which then triggers all prior HTTP calls again and again.
A minor problem is, that this code is ugly and it looks like it's too much for such a simple use case.
Questions:
How to solve this problem in a nice way?
Is is possible to combine those Observables in a different way, without triggering the whole stack again and again?
You didn't include it but I'll assume that you have some way of detecting when the user reaches the bottom of the page. An event that you can use to trigger new loads. For the sake of this answer I'll say that you have defined it somewhere as:
const nextPage = fromEvent(page, 'nextpage');
What you really want to be doing is trying to map this to a stream of one directional flow rather than sort of using the stream as a mutable object. Thus:
const pageStream = nextPage.pipe(
//Always trigger the first page to load
startWith(0),
//Load these pages asynchronously, but keep them in order
concatMap(
(_, pageNum) => from($http(...)).pipe(pluck('content'))
),
//One option of how to join the pages together
scan((pages, p) => ([...pages, p]), [])
)
;
If you need reset functionality I would suggest that you also consider wrapping that whole stream to trigger the reset.
resetPages.pipe(
// Used for the "first" reset when the page first loads
startWith(0),
//Anytime there is a reset, restart the internal stream.
switchMapTo(
nextPage.pipe(
startWith(0),
concatMap(
(_, pageNum) => from($http(...)).pipe(pluck('content'))
),
scan((pages, p) => ([...pages, p]), [])
)
).subscribe(x => /*Render page content*/);
As you can see, by refactoring to nest the logic into streams we can remove the global state that was floating around before
You can use Subject and separate the problem you are solving into 2 observables. One is for scrolling events , and the other is for retrieving data. For example:
let scrollingSubject = new Rx.Subject();
let dataSubject = new Rx.Subject();
//store the data that has been received back from server to check if a page has been
// received previously
let dataList = [];
scrollingSubject.subscribe(function(page) {
dataSubject.onNext({
pageNumber: page,
pageData: [page + 10] // the data from the server
});
});
dataSubject.subscribe(function(data) {
console.log('Received data for page ' + data.pageNumber);
dataList.push(data);
});
//scroll to page 1
scrollingSubject.onNext(1);
//scroll to page 2
scrollingSubject.onNext(2);
//scroll to page 3
scrollingSubject.onNext(3);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.1.0/rx.all.js"></script>

jQuery event trigger; callback and timing with respect to animations

Perhaps my question deviates from the simplicity of itself: Given I .trigger() an event, how can I ensure that code following said .trigger() will not execute until the entire event handler function has completed, including all animations, delays, et al., therein?
I hope I'm missing something here; I'm setting up a UI with a bunch of custom events. Some of the events are really just aggregates of other events; for instance:
// ...
'cb-ui.hide': function(event){
// do stuff to hide
},
'cb-ui.close': function(event){
$(this).trigger('cb-ui.hide');
// more stuff for close
},
// ...
Given there is an animation in the cb-ui.hide event, like .fadeOut(1500), it appears (in my testing) that the remaining // more stuff for close doesn't wait for the animation to complete in the triggered event. I was thinking (previous to referencing the docs) that .trigger() would likely have an optional callback argument much like the animation methods:
$(this).trigger('cb-ui.hide', function(event){
// more stuff for close
});
But this doesn't appear to be the case. Since event triggers are not blocking (or don't appear to be at least), what can I do to force the desired functionality, while keeping with the event handler/trigger implementation that I've been building off of?
More specifically:
$('[data-cb-ui-class="window"]').live({
'cb-ui.hide': function(event){
$(this).find('[data-cb-ui-class="content"]').animate({
opacity: 0
}, 1000);
},
'cb-ui.show': function(event){
$(this).find('[data-cb-ui-class="content"]').animate({
opacity: 1
}, 1000);
}
'cb-ui.close': function(event){
$(this).trigger('cb-ui.hide');
$(this).find('[data-cb-ui-class="content"]').animate({
height: 'hide' // happening simultaneously to the animation of 'cb-ui.hide'
// expected to happen in tandem; one after the other
}, 1000);
},
'cb-ui.update': function(event, html){
// none of this is working as expected; the expected being, the 'cb-ui.hide'
// event is triggered (thus fading the [...-content] out) the HTML content is
// updated, then the [...-content] is faded back in from 'cb-ui.show'
// instead its just a mess that results in it fading out
$(this).trigger('cb-ui.hide');
$(this).find('[data-cb-ui-class="content"]').html(html);
$(this).trigger('cb-ui-show');
}
});
$('#foo').trigger('cb-ui.update', ['<p>Hello world!</p>']); // #foo is bound
This example animation should take ~2 seconds, but appears to be taking 1; both animations are occurring simultaneous to each other, rather than in logical order.
Not sure if I understand your question right, but does this make sense?
You can just pass another function to be run after the animation is done.
'cb-ui.hide': function(event, callback){
$('.lol').fadeTo(0,function() {
// fire callback
})
},
'cb-ui.close': function(event){
cb-ui.hide(e,function() {
// other stuff
});
},