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>
Related
I am a starter at React! Started last week ;)
My first project is to create a timer which has a reset function and a second count function.
The reset function is working great, however the timer does not. Which is the best way to do it? It should increase +1s on variable 'second' according to the setTimeout() function.
Is it possible to create a loop on Hooks? I tried to do with the code below, but the page goes down, I think it is because the infinite loop that the code creates;
const [hour, setHour] = useState(4)
const [minute, setMinute] = useState(8)
const [second, setSecond] = useState(12)
// methods
const setTime = (value: string) => {
if (value === 'reset'){
setHour(0);
setMinute(0);
setSecond(0);
}
}
const startTime = () => {
while (second < 60){
setTimeout(() => {
setSecond(second + 1);
}, 1000);
}
};
<div className="d-flex justify-content-center">
<MainButton
variantButton="outline-danger"
textButton="RESET"
functionButton={() => setTime('reset')}
/>
<MainButton
variantButton="outline-success"
textButton="START"
functionButton={() => startTime()}
/>
</div>
Welcome to React! You're very close. setTimeout and setInterval are very similar and for this you can simply use setInterval. No need for a while() loop! Check out this working Sandbox where I created a simple React Hook that you can use in your App.js
https://codesandbox.io/s/recursing-hooks-jc6w3v
The reason your code got caught in an infinite loop is because startTime() function has stale props. Specifically, the second variable is always 0 in this case, because when you defined startTime() on component mount, second was 0. The function doesn't track it's incrementing.
To resolve this issue, instead of:
setSecond(second + 1);
Try using:
setSecond((s) => s += 1);
EDIT* There are many good articles on React Stale Props. Here's one that's helpful: https://css-tricks.com/dealing-with-stale-props-and-states-in-reacts-functional-components/
EDIT** Additional inline examples of the exact issue:
Two changes I would make:
Use setInterval instead of setTimeout in a while() loop.
Create a useTimer hook which handles your timer logic.
App.js
import "./styles.css";
import useTimer from "./useTimer";
export default function App() {
const [setTime, startTime, stopTime, hour, minute, second] = useTimer();
return (
<div>
<div className="d-flex justify-content-center">
<button onClick={() => setTime("reset")}>RESET</button>
<button onClick={startTime}>START</button>
<button onClick={stopTime}>STOP</button>
</div>
<br />
<div>
Hour: {hour} <br />
Minute: {minute} <br />
Second: {second} <br />
</div>
</div>
);
}
useTimer.js
import { useState } from "react";
const useTimer = () => {
const [hour, setHour] = useState(4);
const [minute, setMinute] = useState(8);
const [second, setSecond] = useState(12);
const [timer, setTimer] = useState();
// methods
const clearTimer = () => clearInterval(timer);
const setTime = (value) => {
if (value === "reset") {
setHour(0);
setMinute(0);
setSecond(0);
}
};
const startTime = () => {
if (timer) clearTimer();
const newInterval = setInterval(() => {
setSecond((s) => (s += 1));
}, 1000);
setTimer(newInterval);
};
const stopTime = () => clearTimer();
return [setTime, startTime, stopTime, hour, minute, second];
};
export default useTimer;
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 am trying to build a timer with three buttons, a start, stop, where it stops on the current integer, and a reset. I have added my code below which results in the following issues.
I thought my stop function would stop the timer from decrementing but it continues to do so. Also, when logging my timer state in the console, you can see it does not update in the console even though it is updating in the DOM. Why is this?
Thank you for any insight at all.
import React from 'react';
import './style.css';
export default function App() {
const [timer, setTimer] = React.useState(50);
const reset = () => {
setTimer(50);
};
const start = () => {
setTimer((prev) => prev - 1);
};
// const interval = setInterval(() => {
// console.log(updated)
// //start() }, 1000)
// }
const interval = () => {
setInterval(() => {
console.log('updated');
console.log(timer);
start();
}, 1000);
};
const stop = () => {
clearInterval(start);
};
return (
<div>
<h1>{timer}</h1>
<button onClick={interval}>start</button>
<button onClick={stop}>stop</button>
<button onClick={reset}>reset</button>
</div>
);
}`
You have a small problem with assigning the actual value for the interval.
Here is how it should be in usage
const interval = setInterval(() => {})
clearInterval(interval)
For your code change, you can create a ref to keep the interval variable and use it to clean up the interval later.
function App() {
const [timer, setTimer] = React.useState(5);
const intervalRef = React.useRef(); //create a ref for interval
const reset = () => {
setTimer(5);
};
const start = () => {
setTimer((prev) => {
if(prev === 0) {
stop();
return 0;
}
return prev - 1;
});
};
// const interval = setInterval(() => {
// console.log(updated)
// //start() }, 1000)
// }
const interval = () => {
//assign interval ref here
intervalRef.current = setInterval(() => {
start();
}, 1000);
};
const stop = () => {
//clear the interval ref
clearInterval(intervalRef.current);
};
return (
<div>
<h1>{timer}</h1>
<button onClick={interval}>start</button>
<button onClick={stop}>stop</button>
<button onClick={reset}>reset</button>
</div>
);
}
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"></div>
clearTimeout or clearInterval each take a token, which was returned by a previous call to setTimeout or setInterval. So you'll need to store that token:
const id = setInterval(() => console.log("triggered"), 1000);
// ...
clearInterval(id)
Also, you should be careful about what happens if App is re-rendered, so you should probably put the set/clear logic inside useEffect so you can cleanup your interval.
Also also, although you didn't ask, your console.log(timer) isn't going to work, and will always print 50. The timer variable inside that callback is captured once, and is never updated because that callback is just inside the setInterval now. You'll need to clear and reset your interval with an updated callback function every time App re-renders, or use a ref that you keep updated, which is a pain.
I would recommend borrowing this custom hook that considers all of these things for you: https://usehooks-ts.com/react-hook/use-interval
Then your App component could become extremely simple, but still be robust:
const { useEffect, useRef, useState, useLayoutEffect } = React;
// https://usehooks-ts.com/react-hook/use-interval
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
useLayoutEffect(() => {
savedCallback.current = callback;
}, [callback])
useEffect(() => {
if (!delay && delay !== 0) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
function App() {
const [timer, setTimer] = useState(50);
const [running, setRunning] = useState(false);
useInterval(() => setTimer(t => t - 1), running ? 1000 : null);
const start = () => setRunning(true);
const stop = () => setRunning(false);
const reset = () => { setTimer(50); };
return (
<div>
<h1>{timer}</h1><button onClick={start}>start</button>
<button onClick={stop}> stop </button>
<button onClick={reset}> reset </button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("react"));
<div id="react"></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>
I think you should not wrap your setTimeout into a callable. Then you lose the ability to start and stop it because the variable does not reference the interval but the callable that wraps the interval.
Take a look at this guide: https://www.w3schools.com/jsref/met_win_clearinterval.asp
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>
when I click to to change code I see inly consols.log. I try to understand it but I can't find the answer..
function App() {
const [laps, setLaps] = useState(0);
const [running, setRunning] = useState(false);
const startTime = Date.now() - laps;
useEffect(() => {
function interval() {
setInterval(() => {
setLaps(Date.now() - startTime);
}, 100);
}
if (!running) {
clearInterval(interval);
console.log('ok');
} else {
interval();
console.log('no');
}
console.log(running);
}, [running]);
return (
<div className="App">
<label>Count: {laps}</label>
<button onClick={() => setRunning(!running)}>{running ? 'stop' : 'start'}</button>
<button>Clear</button>
</div>
);
}
In clean JavaScript this code should works correctly(of course without JSX)?
clearInterval expects a number as argument that is returned from setInterval, but you are giving it the interval function as argument.
You could instead just create the interval if running is true, and return a cleanup function from useEffect that will be run the next time the effect is run.
const { useState, useEffect } = React;
function App() {
const [laps, setLaps] = useState(0);
const [running, setRunning] = useState(false);
const startTime = Date.now() - laps;
useEffect(
() => {
if (running) {
const interval = setInterval(() => {
setLaps(Date.now() - startTime);
}, 100);
return () => clearInterval(interval);
}
},
[running]
);
return (
<div className="App">
<label>Count: {laps}</label>
<button onClick={() => setRunning(!running)}>
{running ? "stop" : "start"}
</button>
<button>Clear</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>