Related
Let's say I have a state called count.
const [count, setCount] = useState();
Now let's say I would like to increase count with 1 every time some key in the keyboard is being pressed.
So I can use useEffect hook and add an event listener to it.
useEffect(() => {
document.addEventListener("keydown", increaseCount);
}, []);
The function increaseCount, increasing count with 1
const increaseCount = () => {
setCount(prevCount => prevCount + 1);
};
Well everything is working great. BUT, if I want to get the current value of count inside increaseCount, I can't do this! The event listener is only called once when the component is mounting (because the useEffect has an empty dependency array).
And if I add count to the dependency array, I have a new problem - I'm creating a kind of loop, because useEffect will call increaseCount, that will call setCount(), that will cause the component to re-render, which will call useEffect again and so on and so on.
I have this kind of problem on a few projects I'm currently working on, and it is very frustrating. So if you know how to answer this - thanks! :)
snippets
When using an empty dependency array and login count inside increaseCount, count will always be 0:
// Get a hook function
const {useState, useEffect} = React;
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.addEventListener("keydown", increaseCount);
}, []);
const increaseCount = () => {
setCount((prevCount) => prevCount + 1);
console.log(count);
};
return (
<div>
count = {count}
</div>
);
};
// Render it
ReactDOM.render(
<Counter />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
and when adding count to the dependency array, we see this "loop" thing happening:
// Get a hook function
const {useState, useEffect} = React;
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.addEventListener("keydown", increaseCount);
}, [count]);
const increaseCount = () => {
setCount((prevCount) => prevCount + 1);
console.log(count);
};
return (
<div>
count = {count}
</div>
);
};
// Render it
ReactDOM.render(
<Counter />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
What you should do really depends on what you need to do. See X Y problem.
As mentioned there are multiple use cases you are trying for.
const { useState, useEffect, useMemo, Fragment } = React;
const App = () => {
const [count, setCount] = useState(0);
function increaseCount(ev) {
setCount((count) => {
const newCount = count + 1;
// You can use the newCount here for something basic if needed...
console.log(newCount);
// I'm not positive, but I'm farily sure that setting other state
// from within a set state function might be problematic.
// If you feel the need to do something with side effects here,
// Consider another useEffect as below.
return newCount;
});
}
useEffect(() => {
document.addEventListener("keydown", increaseCount);
// Always clean up after your effect!
return () => document.removeEventListener("keydown", increaseCount);
// If you use `increaseCount` here as a dependency (which you should), the function will get recreated every time.
// Or you could set it up by creating the `increase count function inside the effect itself
}, [increaseCount]);
useEffect(() => {
// If you need to use newCount for something more complicated, do it here...
// Or for side effects
}, [count]);
const arrayCountSized = useMemo(() => {
// You can use the count here in a useMemo for things that are
// derived, but not stateful in-of-themselves
return new Array(count).fill(null).map((_, idx) => idx);
}, [count]);
return (
<Fragment>
<div>{count}</div>
<ul>
{arrayCountSized.map((row) => (
<li key={row}>{row}</li>
))}
</ul>
</Fragment>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"/>
As for the situation you mentioned in particular, here's one solution.
You can get the "current Index" from the array length normally, which is what I'm doing here.
If the "currentIndex" is more complicated than that and inexorably tied together with the array state, you should useReducer to set up that tied state in a pure fashion.
You could also use a reference to the array in your listener function.
const arrayRef = useRef(array);
useEffect(()=>{arrayRef.current=array},[array]);
// arrayRef.current is safe to use in any effect after this.
const { useState, useEffect, useMemo, Fragment } = React;
const App = () => {
const [array, setArray] = useState([]);
const curIndex = array.length - 1;
useEffect(() => {
function addKey(ev) {
if (ev.key.length === 1 && ev.key >= "a" && ev.key <= "z") {
setArray((arr) => [...arr, ev.key]);
}
}
document.addEventListener("keydown", addKey);
// Always clean up after your effect!
return () => document.removeEventListener("keydown", addKey);
}, []);
return (
<Fragment>
<div>Current Index: {curIndex}</div>
<div>Keys pressed: </div>
<ul>
{array.map((row, index) => (
<li key={index}>{row}</li>
))}
</ul>
</Fragment>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"/>
useReducer Approach:
As I don't have enough details, this is the same solution as above, just in a reducer.
const { useState, useEffect, useReducer, useMemo, Fragment } = React;
function reducer(state, key) {
if (!key) {
return state;
}
const array = [...state.array, key];
return { array, currentIndex: array.length - 1 };
}
const App = () => {
const [{ array, currentIndex }, addKey] = useReducer(reducer, {
array: [],
currentIndex: -1,
});
useEffect(() => {
function keydownListener(ev) {
if (ev.key.length === 1 && ev.key >= "a" && ev.key <= "z") {
addKey(ev.key);
}
}
document.addEventListener("keydown", keydownListener);
// Always clean up after your effect!
return () => document.removeEventListener("keydown", keydownListener);
}, []);
return (
<Fragment>
<div>Current Index: {currentIndex}</div>
<div>Keys pressed: </div>
<ul>
{array.map((row, index) => (
<li key={index}>{row}</li>
))}
</ul>
</Fragment>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"/>
I want to subtract the value of 'percent' from the function by 0.25.
However, subtraction does not work.
I used setState, but I don't know why it doesn't work.
import React, {useState, useRef, useCallback} from 'react';
const Ques = () => {
const [percent,setPercent] = useState(1);
const intervalRef = useRef(null);
const start = useCallback(() =>{
if (intervalRef.current !== null){
return;
}
intervalRef.current = setInterval(()=>{
if (percent > 0){
setPercent(c => c - 0.25);
console.log("percent = ", percent);
}
else {
setPercent(c => 1);
}
}, 1000);
}, []);
return (
<div>
<button onClick={()=>{start()}}>{"Start"}</button>
</div>
);
}
export default Ques;
Issue
The enqueued state updates are working correctly but you've a stale enclosure over the percent state in the interval callback that you are logging, it never will update.
Solution
If you want to log the percent state then use an useEffect hook to log changes.
const Ques = () => {
const [percent, setPercent] = useState(1);
const intervalRef = useRef(null);
useEffect(() => {
console.log("percent = ", percent); // <-- log state changes here
}, [percent]);
const start = useCallback(() => {
if (intervalRef.current !== null) {
return;
}
intervalRef.current = setInterval(() => {
setPercent((c) => Math.max(0, c - 0.25)); // <-- simpler updater function
}, 1000);
}, []);
return (
<div>
Percent: {percent * 100}
<button onClick={start}>Start</button>
</div>
);
};
You can create a ref for percent also and chenge its current value as:
codesandbox link
import React, { useRef, useCallback } from "react";
const Ques = () => {
const percentRef = useRef(1);
const intervalRef = useRef(null);
const start = useCallback(() => {
if (intervalRef.current !== null) {
return;
}
intervalRef.current = setInterval(() => {
console.log("percent = ", percentRef.current);
percentRef.current > 0
? (percentRef.current -= 0.25)
: (percentRef.current = 1);
}, 1000);
}, []);
return (
<div>
<button onClick={start}>Start</button>
</div>
);
};
export default Ques;
I think useCallback and useRef is not a good fit. Below is a minimal verifiable example using useState and useEffect. Note this function appropriately performs cleanup on the timer when the component is unmounted. Click Run to run the code snippet and click start to begin running the effect.
function App() {
const [percent, setPercent] = React.useState(1)
const [running, setRunning] = React.useState(false)
React.useEffect(() => {
if (!running) return
const t = window.setTimeout(() => {
setPercent(c => c > 0 ? c - 0.25 : 1)
}, 1000)
return () => window.clearTimeout(t)
}, [running, percent])
return <div>
<button onClick={() => setRunning(true)} children="start" />
<pre>percent: {percent}</pre>
</div>
}
ReactDOM.render(<App/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
This may be one possible solution to achieve what is presumed to be the desired objective:
Code Snippet
const {useState, useRef, useCallback} = React;
const Ques = () => {
const [percent,setPercent] = useState(1);
const intervalRef = useRef(null);
const start = useCallback((flag) => {
if (intervalRef.current !== null){
if (flag && flag === 'end') clearInterval(intervalRef.current);
return;
}
intervalRef.current = setInterval(() => {
setPercent(
prev => (prev > 0 ? prev - 0.25 : 1)
);
}, 1000);
}, []);
return (
<div>
percent: {percent} <br/> <br/>
<button onClick={() => start('bgn')}>Start</button>
<button onClick={() => start('end')}>Stop</button>
</div>
);
}
ReactDOM.render(
<div>
<h3>DEMO</h3>
<Ques />
</div>,
document.getElementById('rd')
);
<div id='rd' />
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
Explanation
There are two buttons Start and Stop
Both invoke the same start method, but with different params (flag)
If intervalRef is already set (ie, not null) and flag is end, clear the interval
The percent is added to the UI to see real-time changes to its value
setPercent is modified to use prev (which holds the correct state)
I need to reset my custom timer to "0 seconds" when I click the Reset button. But when I press Start my timer continues from last value, not from "0 seconds".
const [time, setTime] = useState (0);
const [timerOn, setTimerOn ] = useState (false);
let observable$ = interval(1000);
let subscription = observable$.subscribe(result =>{
if (timerOn) {
setTime(result);
}
});
return () => subscription.unsubscribe();
}, [timerOn]);
return (
<div>
{!timerOn && (
<button onClick={() => setTimerOn(true)}>Start</button>
)}
{ time > 0 && (
<button onClick={() => setTime(0)}>Reset</button>
)}
Problem
Let's take a look at the Reset button:
<button onClick={() => setTime(0)}>Reset</button>
When you click this button it runs the following function:
() => setTime(0)
This just sets the time state back to 0. That's all. It doesn't touch the subscription to the interval observable at all. As a result, the interval subscription will continue emitting numbers in sequence which is why it appears to continue from the last value.
Solution
Your reset function will have to do more than just setting time back to 0. What it does exactly will be up to your specific use case. For example, it could end the subscription, end the subscription and create a new one, or reset the existing subscription. I've included a code example for a basic way of ending the subscription:
const {
StrictMode,
useCallback,
useRef,
useState,
} = React;
const { interval } = rxjs;
const rootElement = document.getElementById("root");
function useCounter() {
const [time, setTime] = useState(0);
const [timerOn, setTimerOn] = useState(false);
const observable$ = interval(1000);
const subscription = useRef();
const start = useCallback(() => {
setTimerOn(true);
subscription.current = observable$.subscribe((result) => {
console.log(result);
setTime(result);
});
});
const stop = useCallback(() => {
setTimerOn(false);
subscription.current.unsubscribe();
});
const reset = useCallback(() => {
setTime(0);
setTimerOn(false);
subscription.current.unsubscribe();
});
return {
time,
timerOn,
start,
stop,
reset
};
}
function Counter() {
const { time, timerOn, start, stop, reset } = useCounter();
return (
<div>
<h1>{time}</h1>
<br />
{!timerOn && <button onClick={() => start()}>Start</button>}
{time > 0 && <button onClick={() => reset()}>Reset</button>}
</div>
);
}
function App() {
return <Counter />;
}
ReactDOM.render(
<StrictMode>
<App />
</StrictMode>,
rootElement
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/rxjs#^7/dist/bundles/rxjs.umd.min.js"></script>
I am building a simple timer app and keeping track of elapsed time in state using hooks. I know I am setting state correctly because it displays in the app every passing second. However, when I console log elapsedTime, it repeatedly logs the initial state (0 in this case):
const Timer = () => {
const [elapsedTime, setElapsedTime] = React.useState(0);
const [totalTime, setTotalTime] = React.useState(0);
const handleStart = () => {
const startTime = Date.now();
setInterval(() => {
const et = Math.floor((Date.now() - startTime) / 1000);
setElapsedTime(et);
console.log(elapsedTime);
}, 1000);
};
const handleStop = () => {
clearInterval();
};
return (
<div className='container'>
<div className='timer'>
<div className='title'></div>
<div className='time'>{elapsedTime}</div>
<button onClick={handleStart}>Start</button>
<button onClick={handleStop}>Stop</button>
</div>
</div>
);
};
ReactDOM.render(<Timer />, document.getElementById("root"));
<div id="root"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
Why is my change in state not being reflected in the console.log I call on line 9?
setState is asynchronous, you won't see the changes in the scope of the function
If you want to see the new values at the place where you put the console.log you can only do console.log(et);
You can also use a useEffect to see the changes of the variable in the console
useEffect(() => {
console.log("elapsedTime", elapsedTime);
}, [elapsedTime]);
Because of javascript clojure, elapsedTime on line 9 will always reference the value it had when the function was created.
how would you go about stopping the timer?
You have to have a reference to the timer you're creating so that you can stop it. A call to setInverval returns such a reference (a unique ID).
Then you have to make sure you only have a single timer running at any point in time.
Borrowing Marc Charpentier's answer, your working version of the code could look like this:
const Timer = () => {
const [elapsedTime, setElapsedTime] = React.useState(0);
const [totalTime, setTotalTime] = React.useState(0);
const [timerId, setTimerId] = React.useState();
React.useEffect(() => {
console.log("elapsedTime", elapsedTime);
}, [elapsedTime]);
const handleStart = () => {
const startTime = Date.now();
if (timerId === undefined) { // make sure that a timer is not already running
setTimerId(
setInterval(() => {
const et = Math.floor((Date.now() - startTime) / 1000);
setElapsedTime(et);
}, 1000)
);
}
};
const handleStop = () => {
clearInterval(timerId);
setTimerId(undefined);
};
return (
<div className='container'>
<div className='timer'>
<div className='title'></div>
<div className='time'>{elapsedTime}</div>
<button onClick={handleStart}>Start</button>
<button onClick={handleStop}>Stop</button>
</div>
</div>
);
};
ReactDOM.render(<Timer />, document.getElementById("root"));
<div id="root"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
I want to run an interval with a delay for the first time it fires. How can I do this with useEffect? Because of the syntax I've found it difficult to achieve what I want to do
The interval function
useEffect(()=>{
const timer = setInterval(() => {
//do something here
return ()=> clearInterval(timer)
}, 1000);
},[/*dependency*/])
The delay function
useEffect(() => {
setTimeout(() => {
//I want to run the interval here, but it will only run once
//because of no dependencies. If i populate the dependencies,
//setTimeout will run more than once.
}, Math.random() * 1000);
}, []);
Sure it is achievable somehow...
getting started
Consider detangling the concerns of your component and writing small pieces. Here we have a useInterval custom hook which strictly defines the setInterval portion of the program. I added some console.log lines so we can observe the effects -
// rough draft
// read on to make sure we get all the parts right
function useInterval (f, delay)
{ const [timer, setTimer] =
useState(null)
const start = () =>
{ if (timer) return
console.log("started")
setTimer(setInterval(f, delay))
}
const stop = () =>
{ if (!timer) return
console.log("stopped", timer)
setTimer(clearInterval(timer))
}
useEffect(() => stop, [])
return [start, stop, timer != null]
}
Now when we write MyComp we can handle the setTimeout portion of the program -
function MyComp (props)
{ const [counter, setCounter] =
useState(0)
const [start, stop, running] =
useInterval(_ => setCounter(x => x + 1), 1000) // first try
return <div>
{counter}
<button
onClick={start}
disabled={running}
children="Start"
/>
<button
onClick={stop}
disabled={!running}
children="Stop"
/>
</div>
}
Now we can useInterval in various parts of our program, and each one can be used differently. All the logic for the start, stop and cleanup is nicely encapsulated in the hook.
Here's a demo you can run to see it working -
const { useState, useEffect } = React
const useInterval = (f, delay) =>
{ const [timer, setTimer] =
useState(undefined)
const start = () =>
{ if (timer) return
console.log("started")
setTimer(setInterval(f, delay))
}
const stop = () =>
{ if (!timer) return
console.log("stopped", timer)
setTimer(clearInterval(timer))
}
useEffect(() => stop, [])
return [start, stop, timer != null]
}
const MyComp = props =>
{ const [counter, setCounter] =
useState(0)
const [start, stop, running] =
useInterval(_ => setCounter(x => x + 1), 1000)
return <div>
{counter}
<button
onClick={start}
disabled={running}
children="Start"
/>
<button
onClick={stop}
disabled={!running}
children="Stop"
/>
</div>
};
ReactDOM.render
( <MyComp/>
, document.getElementById("react")
)
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script></script>
getting it right
We want to make sure our useInterval hook doesn't leave any timed functions running if our timer are stopped or after our components are removed. Let's test them out in a more rigorous example where we can add/remove many timers and start/stop them at any time -
A few fundamental changes were necessary to make to useInterval -
function useInterval (f, delay = 1000)
{ const [busy, setBusy] = useState(0)
useEffect(() => {
// start
if (!busy) return
setBusy(true)
const t = setInterval(f, delay)
// stop
return () => {
setBusy(false)
clearInterval(t)
}
}, [busy, delay])
return [
_ => setBusy(true), // start
_ => setBusy(false), // stop
busy // isBusy
]
}
Using useInterval in MyTimer component is intuitive. MyTimer is not required to do any sort of cleanup of the interval. Cleanup is automatically handled by useInterval -
function MyTimer ({ delay = 1000, auto = true, ... props })
{ const [counter, setCounter] =
useState(0)
const [start, stop, busy] =
useInterval(_ => {
console.log("tick", Date.now()) // <-- for demo
setCounter(x => x + 1)
}, delay)
useEffect(() => {
console.log("delaying...") // <-- for demo
setTimeout(() => {
console.log("starting...") // <-- for demo
auto && start()
}, 2000)
}, [])
return <span>
{counter}
<button onClick={start} disabled={busy} children="Start" />
<button onClick={stop} disabled={!busy} children="Stop" />
</span>
}
The Main component doesn't do anything special. It just manages an array state of MyTimer components. No timer-specific code or clean up is required -
const append = (a = [], x = null) =>
[ ...a, x ]
const remove = (a = [], x = null) =>
{ const pos = a.findIndex(q => q === x)
if (pos < 0) return a
return [ ...a.slice(0, pos), ...a.slice(pos + 1) ]
}
function Main ()
{ const [timers, setTimers] = useState([])
const addTimer = () =>
setTimers(r => append(r, <MyTimer />))
const destroyTimer = c => () =>
setTimers(r => remove(r, c))
return <main>
<button
onClick={addTimer}
children="Add Timer"
/>
{ timers.map((c, key) =>
<div key={key}>
{c}
<button
onClick={destroyTimer(c)}
children="Destroy"
/>
</div>
)}
</main>
}
Expand the snippet below to see useInterval working in your own browser. Fullscreen mode is recommended for this demo -
const { useState, useEffect } = React
const append = (a = [], x = null) =>
[ ...a, x ]
const remove = (a = [], x = null) =>
{ const pos = a.findIndex(q => q === x)
if (pos < 0) return a
return [ ...a.slice(0, pos), ...a.slice(pos + 1) ]
}
function useInterval (f, delay = 1000)
{ const [busy, setBusy] = useState(0)
useEffect(() => {
// start
if (!busy) return
setBusy(true)
const t = setInterval(f, delay)
// stop
return () => {
setBusy(false)
clearInterval(t)
}
}, [busy, delay])
return [
_ => setBusy(true), // start
_ => setBusy(false), // stop
busy // isBusy
]
}
function MyTimer ({ delay = 1000, auto = true, ... props })
{ const [counter, setCounter] =
useState(0)
const [start, stop, busy] =
useInterval(_ => {
console.log("tick", Date.now())
setCounter(x => x + 1)
}, delay)
useEffect(() => {
console.log("delaying...")
setTimeout(() => {
console.log("starting...")
auto && start()
}, 2000)
}, [])
return <span>
{counter}
<button
onClick={start}
disabled={busy}
children="Start"
/>
<button
onClick={stop}
disabled={!busy}
children="Stop"
/>
</span>
}
function Main ()
{ const [timers, setTimers] = useState([])
const addTimer = () =>
setTimers(r => append(r, <MyTimer />))
const destroyTimer = c => () =>
setTimers(r => remove(r, c))
return <main>
<p>Run in expanded mode. Open your developer console</p>
<button
onClick={addTimer}
children="Add Timer"
/>
{ timers.map((c, key) =>
<div key={key}>
{c}
<button
onClick={destroyTimer(c)}
children="Destroy"
/>
</div>
)}
</main>
}
ReactDOM.render
( <Main/>
, document.getElementById("react")
)
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script></script>
getting advanced
Let's imagine an even more complex useInterval scenario where the timed function, f, and the delay can change -
function useInterval (f, delay = 1000)
{ const [busy, setBusy] = // ...
const interval = useRef(f)
useEffect(() => {
interval.current = f
}, [f])
useEffect(() => {
// start
// ...
const t =
setInterval(_ => interval.current(), delay)
// stop
// ...
}, [busy, delay])
return // ...
}
Now we can edit MyTimer to add the doubler and turbo state -
function MyTimer ({ delay = 1000, auto = true, ... props })
{ const [counter, setCounter] = useState(0)
const [doubler, setDoubler] = useState(false) // <--
const [turbo, setTurbo] = useState(false) // <--
const [start, stop, busy] =
useInterval
( doubler // <-- doubler changes which f is run
? _ => setCounter(x => x * 2)
: _ => setCounter(x => x + 1)
, turbo // <-- turbo changes delay
? Math.floor(delay / 2)
: delay
)
// ...
Then we add a double and turbo button -
// ...
const toggleTurbo = () =>
setTurbo(t => !t)
const toggleDoubler = () =>
setDoubler(t => !t)
return <span>
{counter}
{/* start button ... */}
<button
onClick={toggleDoubler} // <--
disabled={!busy}
children={`Doubler: ${doubler ? "ON" : "OFF"}`}
/>
<button
onClick={toggleTurbo} // <--
disabled={!busy}
children={`Turbo: ${turbo ? "ON" : "OFF"}`}
/>
{/* stop button ... */}
</span>
}
Expand the snippet below to run the advanced timer demo in your own browser -
const { useState, useEffect, useRef, useCallback } = React
const append = (a = [], x = null) =>
[ ...a, x ]
const remove = (a = [], x = null) =>
{ const pos = a.findIndex(q => q === x)
if (pos < 0) return a
return [ ...a.slice(0, pos), ...a.slice(pos + 1) ]
}
function useInterval (f, delay = 1000)
{ const interval = useRef(f)
const [busy, setBusy] = useState(0)
useEffect(() => {
interval.current = f
}, [f])
useEffect(() => {
// start
if (!busy) return
setBusy(true)
const t =
setInterval(_ => interval.current(), delay)
// stop
return () => {
setBusy(false)
clearInterval(t)
}
}, [busy, delay])
return [
_ => setBusy(true), // start
_ => setBusy(false), // stop
busy // isBusy
]
}
function MyTimer ({ delay = 1000, ... props })
{ const [counter, setCounter] =
useState(0)
const [doubler, setDoubler] = useState(false)
const [turbo, setTurbo] = useState(false)
const [start, stop, busy] =
useInterval
( doubler
? _ => setCounter(x => x * 2)
: _ => setCounter(x => x + 1)
, turbo
? Math.floor(delay / 2)
: delay
)
const toggleTurbo = () =>
setTurbo(t => !t)
const toggleDoubler = () =>
setDoubler(t => !t)
return <span>
{counter}
<button
onClick={start}
disabled={busy}
children="Start"
/>
<button
onClick={toggleDoubler}
disabled={!busy}
children={`Doubler: ${doubler ? "ON" : "OFF"}`}
/>
<button
onClick={toggleTurbo}
disabled={!busy}
children={`Turbo: ${turbo ? "ON" : "OFF"}`}
/>
<button
onClick={stop}
disabled={!busy}
children="Stop"
/>
</span>
}
function Main ()
{ const [timers, setTimers] = useState([])
const addTimer = () =>
setTimers(r => append(r, <MyTimer />))
const destroyTimer = c => () =>
setTimers(r => remove(r, c))
return <main>
<p>Run in expanded mode. Open your developer console</p>
<button
onClick={addTimer}
children="Add Timer"
/>
{ timers.map((c, key) =>
<div key={key}>
{c}
<button
onClick={destroyTimer(c)}
children="Destroy"
/>
</div>
)}
</main>
}
ReactDOM.render
( <Main/>
, document.getElementById("react")
)
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script></script>
I think What you're trying to do is this:
const DelayTimer = props => {
const [value, setvalue] = React.useState("initial");
const [counter, setcounter] = React.useState(0);
React.useEffect(() => {
let timer;
setTimeout(() => {
setvalue("delayed value");
timer = setInterval(() => {
setcounter(c => c + 1);
}, 1000);
}, 2000);
return () => clearInterval(timer);
}, []);
return (
<div>
Value:{value} | counter:{counter}
</div>
);
};
// Render it
ReactDOM.render(<DelayTimer />, document.getElementById("react"));
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script></script>
Is this what you want to achieve? the empty array on useeffect tells it will run this code once the element is rendered
const {useState, useEffect} = React;
// Example stateless functional component
const SFC = props => {
const [value,setvalue] = useState('initial')
const [counter,setcounter] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setvalue('delayed value')
setcounter(counter+1)
clearInterval(timer)
}, 2000);
}, []);
return(<div>
Value:{value} | counter:{counter}
</div>)
};
// Render it
ReactDOM.render(
<SFC/>,
document.getElementById("react")
);
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script></script>
If you are trying to use a setInterval inside useEffect, I think you switched up the order a bit, it should be like this
const INTERVAL_DELAY = 1000
useEffect(() => {
const interval = setInterval(() => {
/* do repeated stuff */
}, INTERVAL_DELAY)
return () => clearInterval(interval)
})
The interval will start after a delay, so if you want an interval delay of X seconds to start after Y seconds, you have to actually use a delay in setTimeout as Y - X
const INITIAL_DELAY = 10000
const INTERVAL_DELAY = 5000
useEffect(() => {
let interval
setTimeout(() => {
const interval = setInterval(() => {
/* do repeated stuff */
}, INTERVAL_DELAY)
}, INITIAL_DELAY - INTERVAL_DELAY)
return () => clearInterval(interval)
})