I'm working with an Observable chain and I'm not interested in the next or error values, only when the chain has fully complete. It feels wrong to tap into the subscribe method and supply 2 noops for next and error just so I can provide an onComplete callback.
For Example:
let isRunning = true;
Observable.from([1000, 2000, 3000])
.concatMap(value => Observable.timer(value)
.subscribe(
() => {},
() => {},
() => isRunning = false;
);
What I'd like is something that looks like this
let isRunning = true;
Observable.from([1000, 2000, 3000])
.concatMap(value => Observable.timer(value)
.onComplete(() => isRunning = false);
No, there's no shortcut. However, you can pass null instead of empty functions.
obs$.subscribe(null, null, () => this.isRunning = false);
If you want some kind of trick you could also do
obs$.last().subscribe(() => this.isRunning = false);
This would make sense if, for example, you need access to the last emitted value in the completion handler.
Shortest form would be use observer object.
obs.subscribe({ complete: () => isRunning = false });
Related
Part of a small project I'm working on is the user being able to add tags to items, similarly to StackOveflow. These tags are stored in a database, so obviously I need to call an API to fetch matching tags. To prevent my API from being hit too often, I want to add a debounce method, however none of the examples I've found seem to work. Even lodash's debounce method doesn't work.
I'm currently trying this debounce method in Vue3;
const debounce = (fn: Function, delay: number): Function => {
let timeout: NodeJS.Timeout;
return (...args: any): void => {
clearTimeout(timeout);
timeout = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
};
onMounted(() => {
debounce(() => console.log('hello'), 500);
});
The debounce method itself is called just fine, but the provided callback isn't. Same goes for lodash's debounce method, the method itself is called but whatever method I pass isn't.
Am I missing something obvious?
Edit: I was indeed missing something obvious. The actual use case was this method (no API call yet, wanted to get the debounce method working first);
const handleTagKeyUp = (event: KeyboardEvent): void => {
filteredTags.value = [];
const value = inputTags.value.trim();
if (event.code === 'Enter') {
addTag(value);
return;
}
if (value.length < 3) {
return;
}
const selectedTagNames = selectedTags.value.map((t: Tag) => t.name.toLowerCase());
filteredTags.value = props.tags.filter((t: Tag) => t.name.toLowerCase().includes(value) && ! selectedTagNames.includes(t.name.toLowerCase()));
};
which is called whenever the keyup event is fired. Simply changing it to
const handleTagKeyUp = (event: KeyboardEvent): void => {
filteredTags.value = [];
const value = inputTags.value.trim();
if (event.code === 'Enter') {
addTag(value);
return;
}
if (value.length < 3) {
return;
}
findTags(value);
};
const findTags = debounce((value: string) => {
const selectedTagNames = selectedTags.value.map((t: Tag) => t.name.toLowerCase());
filteredTags.value = props.tags.filter((t: Tag) => t.name.toLowerCase().includes(value) && ! selectedTagNames.includes(t.name.toLowerCase()));
}, 500);
fixed the issue.
This isn't how debounce is used. Think about it: how would debounce respond to multiple calls to onMounted if a new debounce is created every time onMounted is called?
debounce returns a function that must be called, and calls to that returned function are denounced:
// In some scope. I don't know Vue.js
const debounced = _.debounce(() => console.log('hello'), 500);
onMounted(() => {
debounced()
});
Let's say there is a code in place 2
var place2IsReady = true;
In place 1 I need to implement the logic below :
Once place2IsReady value was changed (to true) then display alert('ready!');
Notes:
place2IsReady variable is not available in the scope of place 1.
the code from place 1 gets executed before place 2 gets executed (or there is a race condition).
Solution 1
I believe I can use window.place2IsReady instead and use setTimeout/setInterval in place 1 until I get window.place2IsReady === true.
Any better options? Using Listeners? On the variable change?
P.S. I need to track only first possible change of place2IsReady.
Is there a better way? Thank you.
You can create a listener for the variable change using setTimeout, something like:
let place2IsReady = false;
setReadyListener();
// testing wait 2 seconds to set place2IsReady to true
// so: an alert should occur after 2 seconds
setTimeout(() => place2IsReady = true, 2000);
function setReadyListener() {
const readyListener = () => {
if (place2IsReady) {
return alert("Ready!");
}
return setTimeout(readyListener, 250);
};
readyListener();
}
A more generic listener 'factory' could be:
let place2IsReady = false;
let fromObj = {
place2IsReady: "busy",
done() { this.place2IsReady = "done"; },
};
const listen = changeListenerFactory();
listen(
() => place2IsReady,
() => console.log("place2IsReady") );
listen(
() => fromObj.place2IsReady === "done",
() => console.log("formObj.place2IsReady done!") );
console.log("Listening...");
// test change variables with listeners
setTimeout(() => place2IsReady = true, 1000);
setTimeout(() => fromObj.done(), 3000);
function changeListenerFactory() {
const readyListener = (condition, callback, delay) => {
if (!condition || typeof condition !== "function") { return true; }
if (condition()) {
return callback();
}
setTimeout(() => readyListener(condition, callback, delay), delay);
};
return (condition, callback = () => {}, delay = 250) =>
readyListener(condition, callback, delay);
}
Or maybe using a Proxy (with a set trap) works for you
const readyState = new Proxy({ ready: false }, {
set (target, prop, val) {
console.log(`readyState.ready changed from ${target[prop]} to ${val}`);
target[prop] = val;
}
});
console.log("Waiting for changes ...");
setTimeout(() => readyState.ready = true, 2000);
Assuming you can replace place2IsReady with an object:
place2IsReady = {
state: false,
set ready(value) {
this.state = value
state && place_1_call()
},
get ready() {
return state
}
}
place_1_call = () => {
alert('ready')
}
place2IsReady.ready = true
im trying to do something similar to google docs. It's an autosave system/loop. I'll save a document after a few seconds of inactivity (by calling a save method from the class).
If there is some activity during that few seconds, then I will reset/prolong the timer by another few seconds. This cycle will continue until the save method is being called, of which i will break the loop and wait for another activity to start the loop again.
Here's some pseudo code i came up with:
doc.on('mouse:up', (event) => {
... //<--- Initialize the timer on mouse:up event
... //<--- When time is up, execute the save method
})
doc.on('mouse:down', (event) => {
... //<--- prolong timer initialized in the mouse:up event if it is available.
}
However, i don't think this way of doing is possible as I cant access the same initialized setTimeOut object. Is there a good way to implement this? would love to hear from yall.
There are a few ways to handle this.
Approach: Basic
Cons: Low level, behavior is not as immediately obvious.
const saveDelayMs = 1000;
let timeoutId;
doc.on('mouse:up', (event) => {
timeoutId = setTimeout(save, saveDelayMs);
});
doc.on('mouse:down', (event) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(save, saveDelayMs);
}
Approach: Debounce Abstraction
Pros: Higher level, code more readable
// Trigger 1000ms after last call
const debouncedSave = debounce(save, 1000);
doc.on('mouse:up', (event) => {
debouncedSave();
});
doc.on('mouse:down', (event) => {
debouncedSave();
});
function debounce(fn, debounceDelayMs) {
let timeoutId;
return () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(fn, debounceDelayMs);
};
});
Lodash provides a debounce function, which I recommend over creating your own.
Simply initialize the variable outside of both functions. This way, it will be accessible inside either of them :
let theTimeout = null;
doc.on('mouse:up', (event) => {
theTimeout = setTimeout(...); //<--- Initialize the timer on mouse:up event
})
doc.on('mouse:down', (event) => {
clearTimeout(theTimeout);
theTimeout = setTimeout(...); //<--- prolong timer initialized in the mouse:up event if it is available.
}
You could use a debouncing method (see here: https://davidwalsh.name/javascript-debounce-function), but probably the clearest if you are going to look at multiple types of inputs (say, dragging an object stops saving, but also touch, and also pointers etc..), it's best to use a global flag and use it to toggle the save flag from anywhere in your code, so anybody can trigger the suggestion for saving:
let saveFlag = false;
setInterval(function(){
if( saveFlag === true ){
console.log( 'save' );
saveFlag = false;
} else {
console.log( 'skip save' );
}
}, 3000);
// Now you can trigger a save on any kind of event,
// and expect it to happen within 3 seconds.
document.addEventListener( 'click', e => saveFlag = true);
document.addEventListener( 'touchend', e => saveFlag = true);
document.addEventListener( 'pointerup', e => saveFlag = true);
This will also prevents duplicate timers from potentially being created, and has a maximum delay or 3000ms in my example, with very little overhead.
useEffect(() => {
playLoop();
}, [state.playStatus]);
const playLoop = () => {
if (state.playStatus) {
setTimeout(() => {
console.log("Playing");
playLoop();
}, 2000);
} else {
console.log("Stopped");
return;
}
};
Output:
Stopped
// State Changed to true
Playing
Playing
Playing
Playing
// State Changed to false
Stopped
Playing // This is the problem, even the state is false this still goes on execute the Truthy stalemate
Playing
Playing
I am working on react-native and I want the recursion to stop when the state value becomes false.
Is there any other way I can implement this code I just want to repeatedly execute a function while the state value is true.
Thank you
Rather than having a playStatus boolean, I'd save the interval ID. That way, instead of setting playStatus to false, call clearInterval. Similarly, instead of setting playStatus to true, call setInterval.
// Can't easily use useState here, because you want
// to be able to call clearInterval on the current interval's ID on unmount
// (and not on re-render) (interval ID can't be in an old state closure)
const intervalIdRef = useRef(-1);
const startLoop = () => {
// make sure this is not called while the prior interval is running
// or first call clearInterval(intervalIdRef.current)
intervalIdRef.current = setInterval(
() => { console.log('Playing'); },
2000
);
};
const stopLoop = () => {
clearInterval(intervalIdRef.current);
};
// When component unmounts, clean up the interval:
useEffect(() => stopLoop, []);
The first thing you should do is make sure to clear the timeout when the state changes to stopped or otherwise check the state within the timeout callback function.
But the problem does not seem to be with the setTimeout code only by itself, but rather that this playLoop is also being called too many times. You should add a console.log with a timestamp right at the start of your playLoop to confirm or disprove this. And to find out where it is called from, you could use console.trace.
const playLoop = () => {
console.log(new Date(), ': playLoop called')
console.trace(); // optional
if (state.playSt....
I have a form with a handle function attached to it.
The handle function has a timeout and this is causing some problems.
const timeOut = useRef(null);
const handleSearchChange = (e) => {
// setSearchKey(e.target.value.toLowerCase().trim());
clearTimeout(timeOut.current);
timeOut.current = setTimeout(() => {
setSearchKey(e.target.value.toLowerCase().trim());
}, 500);
}
If I console.log(e.target.value) outside the settimeout function it works fine, when i incorporate the setTimeout function it breaks. Why is this?
I tried simplifying the function to just this :
const handleSearchChange = (e) => {
// setSearchKey(e.target.value.toLowerCase().trim());
console.log(e.target.value)
setTimeout(() => {
// setSearchKey(e.target.value.toLowerCase().trim());
console.log(e.target.value)
}, 500);
}
The issue stays..It logs the first console.log and at the second it breaks.
Event values are cleared by react. You either need to use event.persist to persit event values or store the values from event to be used later
According to react documentation:
SyntheticEvent object will be reused and all properties will be
nullified after the event callback has been invoked. This is for
performance reasons. As such, you cannot access the event in an
asynchronous way.
const handleSearchChange = (e) => {
// setSearchKey(e.target.value.toLowerCase().trim());
clearTimeout(timeOut.current);
const value = e.target.value.toLowerCase().trim();
timeOut.current = setTimeout(() => {
setSearchKey(value);
}, 500);
}
That’s because the e event object in react is a synthetic event object produced by react, not the native event object produced by browser internal.
In order to prevent allocation of new objects all the time, it’s designed to be a reusable object, which means its properties are stripped after emission and re-assigned for next event.
So for your case, because you revisited this object in async callback after emission, it’s been "recycled", making it’s properties outdated. To solve this problem, you can save up beforehand the desired value in the sync event loop, then pass it to async callback.
handleSearchChange = (e) => {
const value = e.target.value.toLowerCase().trim()
clearTimeout(timeOut.current);
timeOut.current = setTimeout(() => {
setSearchKey(value);
}, 500);
}