I have a toggle button in a react component:
toggleSpeak = () => {
this.setState({buttonOn: !this.state.buttonOn});
}
And the button changes its style depending on its state:
<img key="headphones" className={audioclass} src={this.state.buttonOn ? white : black} onClick={this.toggleSpeak}/>
This also triggers some stuff in a child component:
play={this.state.buttonOn}
This triggers some speechSynthesis playback, which sometimes takes a while. The problem is that I want the user to realize that something is happening right away. The button, however, doesn't change its style right away. As long as I'm triggering something else, whether it is through a passthrough property to the child as above, or through triggering a redux action, it still delays changing color for a few seconds.
I want to change color right away without delay, so the user knows not to keep repushing it. How can I accomplish this?
this.setState({}) function is indeed asynchronous so what you are claiming is likely to be true for a very short number of milliseconds considering that all you have in the trigger is
toggleSpeak = () => {
this.setState({buttonOn: !this.state.buttonOn});
}
The noticeable delay you speak of should be unnoticeable. I would think that the delay is being imposed from elsewhere. (say you require some other synchronous code to run before this.setState({}). Do show us more of the relevant code so that we can get better grasp of what's happening.
Are you doing the speechSynthesis in render?
You should call the function that does the speechSynthesis after toggling the button.
As far as UX is concerned, I would recommend that you show a loading indicator while you are doing a task that might take some time to finish. Also, you could disable the button until the speechSynthesis is finished.
toggleSpeak = () => {
if(!this.state.doingSpeechSynthesis) {
this.setState(
{buttonOn: !this.state.buttonOn, doingSpeechSynthesis: true},
() => speechSynthesis(args, this.setState{doingSpeechSynthesis: false}));
}
}
I'm not sure if this is the "react" way of doing things, but I came up with a solution that works. I split up the property I pass to turn on the player from the button toggle.
state = {
buttonOn: false,
play: false
}
Button attributes are the same as above, changing with the buttonOn state.
ChildComponent property:
... play={this.state.play}
Then, on the button toggle event I wait a half a sec before I change the play state. This is so the button will update it's style right away, and then all the player stuff can run after a tick.
togglePlay = (newValue) => {
this.setState({play: newValue});
}
toggleSpeak = (e) => {
let newValue = !this.state.buttonOn;
this.setState({buttonOn: newValue});
if (this.state.play != newValue) {
setTimeout(function() {
this.togglePlay(newValue);
}.bind(this), 500);
}
Then of course clear the timeout function on dismount:
componentWillUnmount() {
clearTimeout(this.togglePlay);
}
Related
I have a draggable-object from Ember drag-and-drop library (https://github.com/mharris717/ember-drag-drop) that default behaves like dragStart is performed instantly. I need to force that object, to start drag when user holds mouse button down for some time, and I thought that I can do it in dragStartAction method delivered by drag-and-drop library, but this solution don't work as expected. its still instant dragStart.
draggable-object from my hbs:
{{#draggable-object
dragHandle=".js-dragHandle"
overrideClass=classes
tagName="tbody"
content=this
dragStartAction=(action "onDragStart")
dragStartHook=(action "onDragStartHook")
dragEndHook=(action "onDragEndHook")
dragEndAction=(action "onDragEnd")
}}
onDragStart action:
#action
onDragStart(componentInstance, data) {
setTimeout(() => {
if (!this.selectedRowIds.includes(this.transaction.id)) {
this.send("selectRow", this.transaction.id);
}
this.selectCoupledVoidPayments();
this.transferTransactions.onDragStart(findComponentObject(componentInstance), this.selectedRowIds, data);
}, 500);
}
onDragStartHook action:
#action
onDragStartHook(event: Event) {
this.transferTransactions.onDragStartHook(event);
}
I was thinking that I can resolve that by using somehow custom delay helper like this, but this is prohibited in my place, and I don't even know that this will do the job.
{{#draggable-object
dragHandle=".js-dragHandle"
overrideClass=classes
tagName="tbody"
content=this
dragStartAction=(action (delay "onDragStart"))
dragStartHook=(action "onDragStartHook")
dragEndHook=(action "onDragEndHook")
dragEndAction=(action "onDragEnd")
}}
What am I doing wrong, or what is the correct way to implement delay in Ember drag-and-drop?
Let me explain the goal of my code first. I have a react component called "Tile" containing a sub-component called "TileMenu" which shows up when I make a right click on my Tile, calling the function "openMenu". I wanted to have two ways of closing it:
clicking somewhere else
waiting some time
But, I also wanted it to stay in place if the mouse was over it. So I needed a function to cancel the timer, which I called "keepMenuOpened". If I moved my mouse away, openMenu() was called again to relaunch the timer.
Here is my code:
import TileMenu from './TileMenu'
function Tile() {
const [openedMenu, setOpenedMenu] = useState(false);
// state used to display —or not— the TileMenu component
const [timeoutID, setTimeoutID] = useState(null);
// state to store timeout ID and clear it
function openMenu() {
// Actually open TileMenu
setOpenedMenu(true);
// Prepare TileMenu closing
window.onclick = closeMenu;
// first case: click somewhere else
setTimeoutID(setTimeout(closeMenu, 3000));
// second case: time out
console.log('open', timeoutID);
}
function closeMenu() {
setOpenedMenu(false);
window.onclick = null;
console.log('close', timeoutID);
clearTimeout(timeoutID);
}
function keepMenuOpened() {
console.log('keep', timeoutID);
clearTimeout(timeoutID);
}
return(
<>
{openedMenu &&
<TileMenu
onMouseOver={keepMenuOpened} onMouseLeave={openMenu} // These two props are passed on to TileMenu component
/>}
<textarea
onContextMenu={openMenu}
>
</textarea>
</>
);
}
export default Tile
At first, it seemed to work perfectly. But I noticed that when I opened, then closed manually, and finally opened my TileMenu again, the delay it took to close a second time (this time alone) was calculated from the first time I opened it.
I used console.log() to see what was happening under the hood and it seemed to be caused by the asynchronous update of states in React (Indeed, at the first attempt, I get open null and close null in the console. When I move my mouse over the TileMenu and then leave it, I get for example open 53, then keep 89 and then open 89 !) If I understand well my specific case, React uses the previous state in openMenu and closeMenu but the current state in keepMenuOpened.
In fact, this is not my first attempt and before using a react state, "timeoutID" was a simple variable. But this time, it was inaccessible inside keepMenuOpened (it logged keep undefined in the console) even if declared in Tile() scope and accessible in openMenu and closeMenu. I think it's because closeMenu is called from openMenu. I found on the net it was called a closure but I didn't figure out exactly how it worked with React.
And now I haven't figured out how to solve my specific problem. I found that I could use useEffect() to access my updated states but it doesn't work in my case where I need to declare my functions inside Tile() to use them as event handlers. I wonder if my code is designed correctly.
The issue here is that you don't reset when opening the menu.
You probably shouldn't store the timer id in state, it seems unnecessary. You also don't clear any running timeouts when the component unmounts, which can sometimes cause issues if you later enqueue state updates or other side-effects assuming the component is still mounted.
It's also considered improper to directly mutate the window.click property, you should add and remove event listeners.
You can use an useEffect hooks to handle both the clearing of the timeout and removing the window click event listener in a cleanup function when the component unmounts.
function Tile() {
const [openedMenu, setOpenedMenu] = useState(false);
const timerIdRef = useRef();
useEffect(() => {
return () => {
window.removeEventListener('click', closeMenu);
clearTimeout(timerIdRef.current);
}
}, []);
function openMenu() {
setOpenedMenu(true);
window.addEventListener('click', closeMenu);
timerIdRef.current = setTimeout(closeMenu, 3000);
}
function closeMenu() {
setOpenedMenu(false);
window.removeEventListener('click', closeMenu);
clearTimeout(timerIdRef.current);
}
function keepMenuOpened() {
clearTimeout(timerIdRef.current);
}
return(
<>
{openedMenu && (
<TileMenu
onMouseOver={keepMenuOpened}
onMouseLeave={openMenu}
/>
)}
<textarea onContextMenu={openMenu} />
</>
);
}
You need to clear previous timer when openMenu called.
function openMenu() {
// clear previous timer before open
clearTimeout(timeoutID);
// Actually open TileMenu
setOpenedMenu(true);
// Prepare TileMenu closing
window.onclick = closeMenu;
// first case: click somewhere else
setTimeoutID(setTimeout(closeMenu, 3000));
// second case: time out
console.log('open', timeoutID);
}
function closeMenu() {
setOpenedMenu(false);
window.onclick = null;
console.log('close', timeoutID);
// timer callback has executed, can remove this line
clearTimeout(timeoutID);
}
In order to get an animation to work, I am trying to update data in a method consecutively, but the updates are happening to fast.
The element in question was previously animated by setting a custom property --int-menu-height to a height, retrieved via a $ref. A transitionend event is then setting that variable to auto. Now to get the same transition into the other direction, I need to first remove the auto and replace it with an interpolatable value, and then set it to zero, so that it animates between those two values. There is another transitionend event waiting, to finish the entire interaction. This is what my code for the closing looks like, right now:
const comp = this;
const menu = this.$refs.menu;
const menuHeight = this.$refs.menuHeight.clientHeight+'px';
// set it too the actual height (from previously 'auto')
this.menuStyles = { '--int-menu-height': menuHeight };
this.$nextTick( () => {
// set it to zero, so that it animates
this.menuStyles = { '--int-menu-height': 0 }
});
// never firing, because no transition
menu.addEventListener('transitionend', function closeMenu() {
comp.isMenuOpen = false;
menu.removeEventListener('transitionend', closeMenu);
});
I'm thought that $nextTick() would do the trick, but it is still happening to fast. How can I update this.menuStyles only after making sure that the previous update has fully rendered through?
Like Radeanu pointed out, I can just use setTimeout(). Because $nextTick() fires after an update in the virtual DOM, not in the real DOM. The code that I use now, that works, looks like this:
setTimeout( () => {
// set it to zero, so that it animates
this.menuStyles = { '--int-menu-height': 0 }
}, 1);
can you please help me to tell is it necessary to clear timeout on button click in react ?
I have a example I want to show a alert on button click.here is my code
https://codesandbox.io/s/competent-torvalds-e84hq?file=/src/App.js
let id;
const onclick = () => {
// first way
id = setTimeout(() => {
alert("---");
}, 0);
// is it required ?
//clearTimeout(id);
// second way without clear
const second = setTimeout(() => {
alert("---");
}, 0);
};
useEffect(() => {
return () => {
console.log("-----", id);
// is it required ?
clearTimeout(id);
};
});
which way is better way ?
If I don't clear timeout on button click is there any performance issue ?.If there any memory leak if I don't clear timeout on button click
You only have to clear the timer if you don't want the timer callback to be called — if you want to cancel the timer callback. In fact, if you uncommented the code you've shown, the callback would be cancelled immediately and the alert would never happen.
If you don't want to cancel the callback, no, there's no need to clear the timer. The resources associated with it are released when the timer call is made; the browser automatically gets rid of its entry in the timer list.
I'm building an app that has a timer to request geolocation while the timer is active. For a timer I'm using react-native-background-timer. Which is kind of working, but not exactly as I want.
With this piece of code:
BackgroundTimer.runBackgroundTimer(() => {
console.log('tic');
},
1000);
the timer is stopping when the app is in the background. While with this piece of code:
const intervalId = BackgroundTimer.setInterval(() => {
console.log('tic');
}, 1000);
it runs constantly even in the background, but I'm not able to stop it. When I run BackgroundTimer.clearInterval(intervalId);, the timer still runs. Even when I leave the screen and go back to my Home screen, the timer still ticks and never stops. This is not ideal, because I need timer to run for a few minutes and then stop.
I set the timer to 1 second to update time left on the screen. I was thinking about setting a timer once for 6 minutes, but then how do I update the state every second? Making 2 timers for this feels like a bad practise, even though it would work.
So to make it more clear, the user suppose to engage in certain activity like walking for a few minutes. So I can't let the timer to stop when user is answering a call or opened a music app to switch music or something else. Timer still needs to run and I need to measure the number of steps and distance with geolocation. It need to work flawlessly even if user opened another app, forgot about my app, and it would still run for the remaining time, then made a record to the database and stopped.
Try the following code snippet,
works both on android and ios
import { DeviceEventEmitter, NativeAppEventEmitter, Platform } from 'react-native';
import _BackgroundTimer from 'react-native-background-timer';
const EventEmitter = Platform.select({
ios: () => NativeAppEventEmitter,
android: () => DeviceEventEmitter,
})();
class BackgroundTimer {
static setInterval(callback, delay) {
_BackgroundTimer.start();
this.backgroundListener = EventEmitter.addListener("backgroundTimer", () => {
this.backgroundTimer = _BackgroundTimer.setInterval(callback, delay);
});
return this.backgroundListener;
}
static clearInterval(timer) {
if (timer) timer.remove();
if (this.backgroundTimer)
_BackgroundTimer.clearInterval(this.backgroundTimer);
_BackgroundTimer.stop();
}
}
export default BackgroundTimer;
Usage
const timer = BackgroundTimer.setInterval(callback, 1000);
BackgroundTimer.clearInterval(timer)
For some reason I got the following error when using #Aravind Vemula's answer.
When calling BackgroundTimer.clearInterval(timer); the following error appears:
timer.remove is not a function
That's why I modified the code slightly.
// parameter removed
static clearInterval() {
// ADD this if statement
if (this.backgroundListener){
this.backgroundListener.remove();
}
if (this.backgroundTimer)
_BackgroundTimer.clearInterval(this.backgroundTimer);
_BackgroundTimer.stop();
}
The above code checks if a backgroundlistener is registered. If yes it removes all listeners and especially our backgroundTimer.
Usage:
BackgroundTimer.clearInterval(); // removed parameter
After my change, everything is working fine on iOS 14.3.
#Aravind Vemula's answer working properly. But if user open the app from the background and timer code is added in background handler then when you stop the timer it is not working. So following changes you need to make in both the methods.
static setInterval(callback, delay) {
if (!this.backgroundListener && !this.locationTimer) {
_BackgroundTimer.start();
this.backgroundListener = EventEmitter.addListener('backgroundTimer', () => {
this.locationTimer = _BackgroundTimer.setInterval(callback, delay);
});
return this.locationTimer;
}
}
static clearInterval() {
if (this.backgroundListener) {
this.backgroundListener.remove();
}
if (this.locationTimer) {
_BackgroundTimer.clearInterval(this.locationTimer);
}
this.backgroundListener = false;
this.locationTimer = false;
_BackgroundTimer.stop();
_BackgroundTimer.start();
}