Trying to make a button event onMouseDown, a function should run at the end of the set amount of time. The function runs onMouseDown and clears the interval onMouseUp, but the interval still runs after releasing the button.
This is the code currently. I have the interval global and set it in the planting function. It should unset in the notPlanting function, but it does not.
import React from "react";
function PlantDefuser() {
var interval
function planting() {
interval = setInterval(() => {
console.log("Defuser Planted")
}, 1000)
}
function notPlanting() {
console.log(interval)
clearInterval(interval)
}
return (
<button onMouseDown={planting} onMouseUp={notPlanting}>Press and Hold</button>
)
}
export default PlantDefuser
This could help you:
useRef allows us to store and update data in the component without triggering a re-render. Now the only re-render happens when the props are updated.
We can store interval in a ref like so
import { useRef } from "react";
const PlantDefuser = () => {
const interval = useRef();
function planting() {
interval.current = setInterval(() => {
console.log("Defuser Planted");
}, 1000);
}
function notPlanting() {
clearInterval(interval.current);
}
return (
<button onMouseDown={planting} onMouseUp={notPlanting}>
Press and Hold
</button>
);
}
export default PlantDefuser
When you declare variables like so in the function component, it is being created on each render. You should be saving the interval id in a state like so:
import React, { useState } from "react";
const PlantDefuser = () => {
const [plantingInterval, setPlantingInterval] = useState(null);
const planting = () => {
const plantingIntervalId = setInterval(() => {
console.log("Defuser Planted");
}, 1000);
setPlantingInterval(plantingIntervalId);
};
const notPlanting = () => {
clearInterval(plantingInterval);
setPlantingInterval(null);
};
return (
<button onMouseDown={planting} onMouseUp={notPlanting}>
Press and Hold
</button>
);
};
export default PlantDefuser;
You might also want to make sure the interval is being cleared when the component unmounts.
You can use useEffect hook with cleanup function to manage the clearInterval method .
like this :
function PlantDefuser() {
const [run, setRun] = useState(false);
useEffect(() => {
if (run) {
const countTimer = setInterval(() => {
console.log("Defuser Planted");
}, 1000);
return () => {
console.log(countTimer);
clearInterval(countTimer);
};
}
}, [run]);
return (
<button onMouseDown={() => setRun(!run)} onMouseUp={() => setRun(!run)}>
Press and Hold
</button>
);
}
export default PlantDefuser;
I don't understand why is when I use setTimeout function my react component start to infinite console.log. Everything is working, but PC start to lag as hell.
Some people saying that function in timeout changing my state and that rerender component, that sets new timer and so on. Now I need to understand how to clear it's right.
export default function Loading() {
// if data fetching is slow, after 1 sec i will show some loading animation
const [showLoading, setShowLoading] = useState(true)
let timer1 = setTimeout(() => setShowLoading(true), 1000)
console.log('this message will render every second')
return 1
}
Clear in different version of code not helping to:
const [showLoading, setShowLoading] = useState(true)
let timer1 = setTimeout(() => setShowLoading(true), 1000)
useEffect(
() => {
return () => {
clearTimeout(timer1)
}
},
[showLoading]
)
Defined return () => { /*code/* } function inside useEffect runs every time useEffect runs (except first render on component mount) and on component unmount (if you don't display component any more).
This is a working way to use and clear timeouts or intervals:
Sandbox example.
import { useState, useEffect } from "react";
const delay = 5;
export default function App() {
const [show, setShow] = useState(false);
useEffect(
() => {
let timer1 = setTimeout(() => setShow(true), delay * 1000);
// this will clear Timeout
// when component unmount like in willComponentUnmount
// and show will not change to true
return () => {
clearTimeout(timer1);
};
},
// useEffect will run only one time with empty []
// if you pass a value to array,
// like this - [data]
// than clearTimeout will run every time
// this value changes (useEffect re-run)
[]
);
return show ? (
<div>show is true, {delay}seconds passed</div>
) : (
<div>show is false, wait {delay}seconds</div>
);
}
If you need to clear timeouts or intervals in another component:
Sandbox example.
import { useState, useEffect, useRef } from "react";
const delay = 1;
export default function App() {
const [counter, setCounter] = useState(0);
const timer = useRef(null); // we can save timer in useRef and pass it to child
useEffect(() => {
// useRef value stored in .current property
timer.current = setInterval(() => setCounter((v) => v + 1), delay * 1000);
// clear on component unmount
return () => {
clearInterval(timer.current);
};
}, []);
return (
<div>
<div>Interval is working, counter is: {counter}</div>
<Child counter={counter} currentTimer={timer.current} />
</div>
);
}
function Child({ counter, currentTimer }) {
// this will clearInterval in parent component after counter gets to 5
useEffect(() => {
if (counter < 5) return;
clearInterval(currentTimer);
}, [counter, currentTimer]);
return null;
}
Article from Dan Abramov.
The problem is you are calling setTimeout outside useEffect, so you are setting a new timeout every time the component is rendered, which will eventually be invoked again and change the state, forcing the component to re-render again, which will set a new timeout, which...
So, as you have already found out, the way to use setTimeout or setInterval with hooks is to wrap them in useEffect, like so:
React.useEffect(() => {
const timeoutID = window.setTimeout(() => {
...
}, 1000);
return () => window.clearTimeout(timeoutID );
}, []);
As deps = [], useEffect's callback will only be called once. Then, the callback you return will be called when the component is unmounted.
Anyway, I would encourage you to create your own useTimeout hook so that you can DRY and simplify your code by using setTimeout declaratively, as Dan Abramov suggests for setInterval in Making setInterval Declarative with React Hooks, which is quite similar:
function useTimeout(callback, delay) {
const timeoutRef = React.useRef();
const callbackRef = React.useRef(callback);
// Remember the latest callback:
//
// Without this, if you change the callback, when setTimeout kicks in, it
// will still call your old callback.
//
// If you add `callback` to useEffect's deps, it will work fine but the
// timeout will be reset.
React.useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// Set up the timeout:
React.useEffect(() => {
if (typeof delay === 'number') {
timeoutRef.current = window.setTimeout(() => callbackRef.current(), delay);
// Clear timeout if the components is unmounted or the delay changes:
return () => window.clearTimeout(timeoutRef.current);
}
}, [delay]);
// In case you want to manually clear the timeout from the consuming component...:
return timeoutRef;
}
const App = () => {
const [isLoading, setLoading] = React.useState(true);
const [showLoader, setShowLoader] = React.useState(false);
// Simulate loading some data:
const fakeNetworkRequest = React.useCallback(() => {
setLoading(true);
setShowLoader(false);
// 50% of the time it will display the loder, and 50% of the time it won't:
window.setTimeout(() => setLoading(false), Math.random() * 4000);
}, []);
// Initial data load:
React.useEffect(fakeNetworkRequest, []);
// After 2 second, we want to show a loader:
useTimeout(() => setShowLoader(true), isLoading ? 2000 : null);
return (<React.Fragment>
<button onClick={ fakeNetworkRequest } disabled={ isLoading }>
{ isLoading ? 'LOADING... ๐' : 'LOAD MORE ๐' }
</button>
{ isLoading && showLoader ? <div className="loader"><span className="loaderIcon">๐</span></div> : null }
{ isLoading ? null : <p>Loaded! โจ</p> }
</React.Fragment>);
}
ReactDOM.render(<App />, document.querySelector('#app'));
body,
button {
font-family: monospace;
}
body, p {
margin: 0;
}
#app {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
button {
margin: 32px 0;
padding: 8px;
border: 2px solid black;
background: transparent;
cursor: pointer;
border-radius: 2px;
}
.loader {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-size: 128px;
background: white;
}
.loaderIcon {
animation: spin linear infinite .25s;
}
#keyframes spin {
from { transform:rotate(0deg) }
to { transform:rotate(360deg) }
}
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
Apart from producing simpler and cleaner code, this allows you to automatically clear the timeout by passing delay = null and also returns the timeout ID, in case you want to cancel it yourself manually (that's not covered in Dan's posts).
If you are looking for a similar answer for setInterval rather than setTimeout, check this out: https://stackoverflow.com/a/59274004/3723993.
You can also find declarative version of setTimeout and setInterval, useTimeout and useInterval, a few additional hooks written in TypeScript in https://www.npmjs.com/package/#swyg/corre.
Your computer was lagging because you probably forgot to pass in the empty array as the second argument of useEffect and was triggering a setState within the callback. That causes an infinite loop because useEffect is triggered on renders.
Here's a working way to set a timer on mount and clearing it on unmount:
function App() {
React.useEffect(() => {
const timer = window.setInterval(() => {
console.log('1 second has passed');
}, 1000);
return () => { // Return callback to run on unmount.
window.clearInterval(timer);
};
}, []); // Pass in empty array to run useEffect only on mount.
return (
<div>
Timer Example
</div>
);
}
ReactDOM.render(
<div>
<App />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
I wrote a react hook to never again have to deal with timeouts.
works just like React.useState():
New answer
const [showLoading, setShowLoading] = useTimeoutState(false)
// sets loading to true for 1000ms, then back to false
setShowLoading(true, { timeout: 1000})
export const useTimeoutState = <T>(
defaultState: T
): [T, (action: SetStateAction<T>, opts?: { timeout: number }) => void] => {
const [state, _setState] = useState<T>(defaultState);
const [currentTimeoutId, setCurrentTimeoutId] = useState<
NodeJS.Timeout | undefined
>();
const setState = useCallback(
(action: SetStateAction<T>, opts?: { timeout: number }) => {
if (currentTimeoutId != null) {
clearTimeout(currentTimeoutId);
}
_setState(action);
const id = setTimeout(() => _setState(defaultState), opts?.timeout);
setCurrentTimeoutId(id);
},
[currentTimeoutId, defaultState]
);
return [state, setState];
};
Old answer
const [showLoading, setShowLoading] = useTimeoutState(false, {timeout: 5000})
// will set show loading after 5000ms
setShowLoading(true)
// overriding and timeouts after 1000ms
setShowLoading(true, { timeout: 1000})
Setting multiple states will refresh the timeout and it will timeout after the same ms that the last setState set.
Vanilla js (not tested, typescript version is):
import React from "react"
// sets itself automatically to default state after timeout MS. good for setting timeouted states for risky requests etc.
export const useTimeoutState = (defaultState, opts) => {
const [state, _setState] = React.useState(defaultState)
const [currentTimeoutId, setCurrentTimeoutId] = React.useState()
const setState = React.useCallback(
(newState: React.SetStateAction, setStateOpts) => {
clearTimeout(currentTimeoutId) // removes old timeouts
newState !== state && _setState(newState)
if (newState === defaultState) return // if already default state, no need to set timeout to set state to default
const id = setTimeout(
() => _setState(defaultState),
setStateOpts?.timeout || opts?.timeout
)
setCurrentTimeoutId(id)
},
[currentTimeoutId, state, opts, defaultState]
)
return [state, setState]
}
Typescript:
import React from "react"
interface IUseTimeoutStateOptions {
timeout?: number
}
// sets itself automatically to default state after timeout MS. good for setting timeouted states for risky requests etc.
export const useTimeoutState = <T>(defaultState: T, opts?: IUseTimeoutStateOptions) => {
const [state, _setState] = React.useState<T>(defaultState)
const [currentTimeoutId, setCurrentTimeoutId] = React.useState<number | undefined>()
// todo: change any to React.setStateAction with T
const setState = React.useCallback(
(newState: React.SetStateAction<any>, setStateOpts?: { timeout?: number }) => {
clearTimeout(currentTimeoutId) // removes old timeouts
newState !== state && _setState(newState)
if (newState === defaultState) return // if already default state, no need to set timeout to set state to default
const id = setTimeout(
() => _setState(defaultState),
setStateOpts?.timeout || opts?.timeout
) as number
setCurrentTimeoutId(id)
},
[currentTimeoutId, state, opts, defaultState]
)
return [state, setState] as [
T,
(newState: React.SetStateAction<T>, setStateOpts?: { timeout?: number }) => void
]
}```
export const useTimeout = () => {
const timeout = useRef();
useEffect(
() => () => {
if (timeout.current) {
clearTimeout(timeout.current);
timeout.current = null;
}
},
[],
);
return timeout;
};
You can use simple hook to share timeout logic.
const timeout = useTimeout();
timeout.current = setTimeout(your conditions)
Trigger api every 10 seconds:
useEffect(() => {
const timer = window.setInterval(() => {
// function of api call
}, 1000);
return () => {
window.clearInterval(timer);
}
}, [])
if any state change:
useEffect(() => {
// add condition to state if needed
const timer = window.setInterval(() => {
// function of api call
}, 1000);
return () => {
window.clearInterval(timer);
}
}, [state])
If your timeout is in the "if construction" try this:
useEffect(() => {
let timeout;
if (yourCondition) {
timeout = setTimeout(() => {
// your code
}, 1000);
} else {
// your code
}
return () => {
clearTimeout(timeout);
};
}, [yourDeps]);
const[seconds, setSeconds] = useState(300);
function TimeOut() {
useEffect(() => {
let interval = setInterval(() => {
setSeconds(seconds => seconds -1);
}, 1000);
return() => clearInterval(interval);
}, [])
function reset() {
setSeconds(300);
}
return (
<div>
Count Down: {seconds} left
<button className="button" onClick={reset}>
Reset
</button>
</div>
)
}
Make sure to import useState and useEffect. Also, add the logic to stop the timer at 0.
If you want to make a button like "start" then using "useInterval" hook may not be suitable since react doesn't allow you call hooks other than at the top of component.
export default function Loading() {
// if data fetching is slow, after 1 sec i will show some loading animation
const [showLoading, setShowLoading] = useState(true)
const interval = useRef();
useEffect(() => {
interval.current = () => setShowLoading(true);
}, [showLoading]);
// make a function like "Start"
// const start = setInterval(interval.current(), 1000)
setInterval(() => interval.current(), 1000);
console.log('this message will render every second')
return 1
}
In case of Intervals to avoid continual attaching (mounting) and detaching (un-mounting) the setInterval method to the event-loop by the use of useEffect hook in the examples given by others, you may instead benefit the use of useReducer.
Imagine a scenario where given seconds and minutes you shall count the time down...
Below we got a reducer function that does the count-down logic.
const reducer = (state, action) => {
switch (action.type) {
case "cycle":
if (state.seconds > 0) {
return { ...state, seconds: state.seconds - 1 };
}
if (state.minutes > 0) {
return { ...state, minutes: state.minutes - 1, seconds: 60 };
}
case "newState":
return action.payload;
default:
throw new Error();
}
}
Now all we have to do is dispatch the cycle action in every interval:
const [time, dispatch] = useReducer(reducer, { minutes: 0, seconds: 0 });
const { minutes, seconds } = time;
const interval = useRef(null);
//Notice the [] provided, we are setting the interval only once (during mount) here.
useEffect(() => {
interval.current = setInterval(() => {
dispatch({ type: "cycle" });
}, 1000);
// Just in case, clear interval on component un-mount, to be safe.
return () => clearInterval(interval.current);
}, []);
//Now as soon as the time in given two states is zero, remove the interval.
useEffect(() => {
if (!minutes && !seconds) {
clearInterval(interval.current);
}
}, [minutes, seconds]);
// We could have avoided the above state check too, providing the `clearInterval()`
// inside our reducer function, but that would delay it until the next interval.
I tried to create a simple timer app with ReactJS and found the below code on the internet.
Does the function that we passed to the useEffect will execute with the dependency change or does it recreates with every dependency change and then execute?
Also I console log the return function of the useEffect and it runs with every render. Does it run only when the component unmount? or with every render?
import { useEffect, useState } from "react";
const App = () => {
const [isActive, setIsActive] = React.useState(false);
const [isPaused, setIsPaused] = React.useState(true);
const [time, setTime] = React.useState(0);
React.useEffect(() => {
let interval = null;
if (isActive && isPaused === false) {
interval = setInterval(() => {
setTime((time) => time + 10);
}, 10);
} else {
clearInterval(interval);
}
return () => {
console.log("cleanup");
clearInterval(interval);
};
}, [isActive, isPaused]);
const handleStart = () => {
setIsActive(true);
setIsPaused(false);
};
const handlePauseResume = () => {
setIsPaused(!isPaused);
};
const handleReset = () => {
setIsActive(false);
setTime(0);
};
return (
<div className="stop-watch">
{time}
<button onClick={handleStart}>start</button>
<button onClick={handlePauseResume}>pause</button>
<button onClick={handleReset}>clear</button>
</div>
);
};
export default App;
The code inside the useEffect hook will run every time a dependency value has been changed. In your case whenever isActive or isPaused changes state.
This means that the reference to the interval will be lost, as the interval variable is redefined.
To keep a steady reference, use the useRef hook to have the reference persist throughout state changes.
const App = () => {
const [isActive, setIsActive] = useState(false);
const [isPaused, setIsPaused] = useState(true);
const [time, setTime] = useState(0);
const interval = useRef(null)
useEffect(() => {
if (isActive && !isPaused) {
interval.current = setInterval(() => {
setTime((time) => time + 10);
}, 10);
} else {
clearInterval(interval.current);
interval.current = null;
}
return () => {
clearInterval(interval.current);
};
}, [isActive, isPaused])
...
}
I am creating a stopwatch in React.js and i am wondering why window.addEventListener('keydown', callback) re-render my component?
import { useEffect, useState } from 'react';
import './App.scss';
import Timer from './Timer';
import Button from './Button';
import Time from './Time';
const App = () => {
const [isRunning, setIsRunning] = useState(false);
const [start, setStart] = useState(new Time(0));
const [stop, setStop] = useState(new Time(0));
const handleStart = () => {
const now = new Date();
setIsRunning(true);
setStart(new Time(now));
setStop(new Time(now));
};
const handleStop = () => {
setIsRunning(false);
setStop(new Time(new Date()));
};
const getTime = () => {
if (isRunning) {
return new Time(new Date().getTime() - start.origin);
} else {
return new Time(stop.origin - start.origin);
}
};
const handleKeyDown = (key) => {
console.log(key.code === 'Space');
};
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
});
return (
<div className="stopwatch">
<Timer getTime={getTime} />
<div className="buttons">
<Button role={'start'} callback={handleStart} />
<Button role={'stop'} callback={handleStop} />
</div>
</div>
);
};
export default App;
When i click start and then stop after let's say 3s. <Timer /> show correctly time that has passed, but then when i press Space on keyboard <Timer /> is re-rendering, showing new time. Then, when i switch my web-browser to VSCode and again to web-browser, <Timer /> isn't re-rendering
Here is my Timer component
import { memo, useEffect, useRef } from 'react';
const Timer = ({ getTime }) => {
const timer = useRef();
console.log('timer rendered');
useEffect(() => {
function run() {
const time = getTime().formatted();
timer.current.textContent = `${time.m}:${time.s}.${time.ms}`;
requestAnimationFrame(run);
}
run();
return () => {
cancelAnimationFrame(run);
};
});
return <div ref={timer} className="timer"></div>;
};
export default memo(Timer);
no matter if I use [] in both or none of useEffect nothing changes.
As #davood-falahati says, adding an empty array as a second argument to useEffect would probably be desirable. From the docs:
... If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesnโt depend on any values from props or state, so it never needs to re-run. This isnโt handled as a special case โ it follows directly from how the dependencies array always works. ...
In your use case:
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []);
I'm attempting to get a timer to call a function quickly at first and then slow down. I have a TimeInterval state that increases and is passed down to my countdown component as a prop
<Countdown
isActive={RandominatorRunning}
target={() => NextSelection()}
timeToChange={TimeInterval}
/>
Countdown Component
import React, { useEffect } from 'react';
const Countdown = ({ isActive, target, timeToChange }) => {
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
target()
}, timeToChange)
} else if (!isActive) {
clearInterval(interval)
}
return () => clearInterval(interval)
}, [isActive])
return null
}
export default Countdown;
My TimeInterval state is working properly and will increase as NextSelection() is called. However this doesn't seem to increase the interval of the countdown component and NextSelection() is always called at the same pace, not at the changing state TimeInterval pace. Why is the countdown component not updating it's pace along with the TimeInterval state?
Not positive this is the best solution, but I was able to alter my countdown component to get the desired effect.
I changed my countdown component to become inactive while it executes the prop update, then resumes as soon as the prop has finished updating.
import React, { useEffect, useState } from 'react';
const Countdown = ({ isActive, target, timeToChange }) => {
const [Active, setActive] = useState(isActive)
const handleTimeExpire = async () => {
await target()
setActive(true)
}
useEffect(() => {
let interval = null;
if (Active) {
interval = setInterval(() => {
setActive(false)
handleTimeExpire()
}, timeToChange)
} else if (!Active) {
clearInterval(interval)
}
return () => clearInterval(interval)
}, [Active])
return null
}
export default Countdown;