I want to start an interval by clicking on a button.
Here the interval gets started but, I can't access the value of counter. because when counter gets equal to 5, the interval should be stoped.
Here is the example:
let interval = null;
const stateReducer = (state, value) => value;
function App(props) {
const [counter, setCounter] = useReducer(stateReducer, 0);
const increment = () => {
interval = setInterval(() => {
setCounter(counter + 1);
if (counter === 5) clearInterval(interval);
console.log(counter);
}, 1000);
};
return (
<div>
<p>{counter}</p>
<button className="App" onClick={increment}>
Increment
</button>
</div>
);
}
You can run this code on codesandbox
change const stateReducer = (state, value) => value; to const
stateReducer = (state, value) => state+value;
make a variable let current_counter = 0; outside function
Change your increment function like this
current_counter = counter;
const increment = () => {
interval = setInterval(() => {
setCounter(1);
if (current_counter === 5) clearInterval(interval);
console.log(current_counter);
}, 1000);
};
Done
import React, { useReducer, useEffect, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
let interval = null;
let current_counter = 0;
const stateReducer = (state, value) => value;
function App(props) {
const [status, setStatus] = useState(false);
const [counter, setCounter] = useReducer(stateReducer, 0);
useEffect(()=>{
if(status){
const interval = setInterval(() => {
setCounter(counter + 1);
}, 1000)
return ()=>{
clearInterval(interval)
};
}
})
const increment = () => {
setStatus(!status)
};
return (
<div>
<p>{counter}</p>
<button className="App" onClick={increment}>
Increment
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Your code has a few problems.
Your reducer declaration
const initialState = { value : 0 }
const reducer = (state, action) =>{
if(action.type === 'INCREMENT') return { value : state.value + 1 }
return state
}
How you're setting your reducer
const [state, dispatch] = useReducer(reducer, initialState)
How you're dispatching your action
intervals are imperative code, you can't consistently declare an interval inside a React's handler without worrying about closure. You could use the click only to flag that the interval should start and handle all imperative code inside an useEffect. Here is a working example
const initialState = { value: 0 };
const reducer = (state, action) => {
if (action.type === "INCREMENT")
return {
value: state.value + 1
};
};
function App() {
const [state, dispatch] = React.useReducer(reducer, initialState);
const [clicked, setClicked] = React.useState(false);
useEffect(() => {
let interval = null;
if (clicked) {
interval = setInterval(() => {
dispatch({ type: "INCREMENT" });
}, 1000);
}
if (state.value > 4) clearInterval(interval);
return () => clearInterval(interval);
}, [clicked, state]);
return <button onClick={() => setClicked(true)}>{state.value}</button>;
}
If your curious about closures and how react handle imperative code take a look on this awesome article from Dan Abramov (the most detailed explanation about effects out there).
Related
I am unable to get the updated prop in setInterval inside Component1, it gives me the old value
following is the code I am using:
import { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
const intervalID = setInterval(() => {
setCounter((counter) => counter + 1);
}, 1000);
return () => {
clearInterval(intervalID);
};
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Component1 counter={counter} />
</div>
);
}
const Component1 = ({ counter }) => {
useEffect(() => {
const intervalID = setInterval(() => {
console.log(counter);
}, 1000);
return () => {
clearInterval(intervalID);
};
}, []);
return <h1>Component1 count: {counter}</h1>;
};
In this code inside Component1, the counter's value is updated after every second on the browser, but on the console.log inside setInterval, I am always getting the initial value not the updated one.
I also get a solution which looks like this
import { useState, useEffect, useRef } from "react";
import "./styles.css";
export default function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
const intervalID = setInterval(() => {
setCounter((counter) => counter + 1);
}, 1000);
return () => {
clearInterval(intervalID);
};
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Component1 counter={counter} />
</div>
);
}
const Component1 = ({ counter }) => {
const counterRef = useRef(null);
counterRef.current = counter;
useEffect(() => {
const intervalID = setInterval(() => {
console.log(counterRef.current);
}, 1000);
return () => {
clearInterval(intervalID);
};
}, []);
return <h1>Component1 count: {counter}</h1>;
};
But in this solution, I have to use extra memory space as I am creating a ref and assigning value to it.
Is there any better solution there to get updated value inside setInterval to this the correct way to do it.
You can try this one in scenario, where you face closure behavior of JavaScript,
useEffect(() => {
const intervalId = setInterval(() => {
console.log('count is', counter);
}, 1000);
return () => clearInterval(intervalId);
}, [counter]);
But in your scenario this will be more suitable in Component1,
useEffect(() => {
console.log(counter);
}, [counter]);
In this case : on first render you set an Interval that print 0 on Console every second
useEffect(() => {
const intervalID = setInterval(() => {
console.log(counter);
}, 1000);
return () => {
clearInterval(intervalID);
};
}, []);
I Edit your codes :
import { useState, useEffect } from "react";
export default function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
const intervalID = setInterval(() => {
setCounter((counter) => counter + 1);
}, 1000);
return () => {
clearInterval(intervalID);
};
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Component1 counter={counter} />
</div>
);
}
const Component1 = ({ counter }) => {
useEffect(() => {
console.log(counter);
}, [counter]);
return <h1>Component1 count: {counter}</h1>;
};
on this case in component1 : when value of counter changed, console.log print new value
useEffect(() => {
console.log(counter);
}, [counter]);
read about useEffect
If you want to execute a method when a property changes. you can use useEffect and you must pass that property in closure like below code :
useEffect(() => {
console.log(counter);
}, [counter]);
I pass counter to useEffect and when it changes, useEffect run console.log with new value
I am creating to-do app in react and for the id of task i am using generator function. But This generator function is giving value 0 everytime and not incrementing the value.I think the reason for issue is useCallback() hook but i am not sure what can be the solution.How to solve the issue?Here i am providing the code :
import DateAndDay, { date } from "../DateAndDay/DateAndDay";
import TaskList, { TaskProps } from "../TaskList/TaskList";
import "./ToDo.css";
import Input from "../Input/Input";
import { ChangeEvent, useCallback, useEffect, useState } from "react";
function ToDo() {
const [inputShow, setInputShow] = useState(false);
const [valid, setValid] = useState(false);
const [enteredTask, setEnteredTask] = useState("");
const [touched, setTouched] = useState(false);
const [tasks, setTasks] = useState<TaskProps[]>(() => {
let list = localStorage.getItem("tasks");
let newdate = String(date);
const setdate = localStorage.getItem("setdate");
if (newdate !== setdate) {
localStorage.removeItem("tasks");
}
if (list) {
return JSON.parse(list);
} else {
return [];
}
});
const activeHandler = (id: number) => {
const index = tasks.findIndex((task) => task.id === id);
const updatedTasks = [...tasks];
updatedTasks[index].complete = !updatedTasks[index].complete;
setTasks(updatedTasks);
};
const clickHandler = () => {
setInputShow((prev) => !prev);
};
const input = inputShow && (
<Input
checkValidity={checkValidity}
enteredTask={enteredTask}
valid={valid}
touched={touched}
/>
);
const btn = !inputShow && (
<button className="add-btn" onClick={clickHandler}>
+
</button>
);
function checkValidity(e: ChangeEvent<HTMLInputElement>) {
setEnteredTask(e.target.value);
}
function* idGenerator() {
let i = 0;
while (true) {
yield i++;
}
}
let id = idGenerator();
const submitHandler = useCallback(
(event: KeyboardEvent) => {
event.preventDefault();
setTouched(true);
if (enteredTask === "") {
setValid(false);
} else {
setValid(true);
const newtitle = enteredTask;
const newComplete = false;
const obj = {
id: Number(id.next().value),
title: newtitle,
complete: newComplete,
};
setTasks([...tasks, obj]);
localStorage.setItem("setdate", date.toString());
setEnteredTask("");
}
},
[enteredTask, tasks, id]
);
useEffect(() => {
const handleKey = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setInputShow(false);
}
if (event.key === "Enter") {
submitHandler(event);
}
};
document.addEventListener("keydown", handleKey);
return () => {
document.removeEventListener("keydown", handleKey);
};
}, [submitHandler]);
useEffect(() => {
localStorage.setItem("tasks", JSON.stringify(tasks));
}, [tasks]);
return (
<div className="to-do">
<DateAndDay />
<TaskList tasks={tasks} activeHandler={activeHandler} />
{input}
{btn}
</div>
);
}
export default ToDo;
useCallBack()'s is used to memorize the result of function sent to it. This result will never change until any variable/function of dependency array changes it's value. So, please check if the dependencies passed are correct or if they are changing in your code or not ( or provide all the code of this file). One of my guess is to add the Valid state as dependency to the array
It's because you are calling the idGenerator outside of the useCallback, so it is only generated if the Component is re-rendered, in your case... only once.
Transfer it inside useCallback and call it everytime the event is triggered:
// wrap this on a useCallback so it gets memoized
const idGenerator = useCallback(() => {
let i = 0;
while (true) {
yield i++;
}
}, []);
const submitHandler = useCallback(
(event: KeyboardEvent) => {
event.preventDefault();
let id = idGenerator();
// ... rest of logic
},
[enteredTask, tasks, idGenerator]
);
If you're using the generated id outside the event handler, store the id inside a state like so:
const idGenerator = useCallback(() => {
let i = 0;
while (true) {
yield i++;
}
}, []);
const [id, setId] = useState(idGenerator());
const submitHandler = useCallback(
(event: KeyboardEvent) => {
event.preventDefault();
let newId = idGenerator();
setId(newId)
// ... rest of logic
},
[enteredTask, tasks, id, idGenerator]
);
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 have gone through some Q&As here but havent been able to understand what I am doing wrong. The following component prints 0s in console and does not update the DOM as expected.
const NotifPopup = ({ notif, index, closeHandler }) => {
const [timer, setTimer] = useState(0);
useEffect(() => {
const timerRef = setInterval(() => {
if (timer === 3) {
clearInterval(timerRef);
closeHandler(index);
} else {
console.log("timer", timer);
setTimer(timer + 1);
}
}, 1000);
}, []); // only run on mount
return (<div className="notifPopup">
<span className=""></span>
<p>{notif.message}</p>
<span className="absolute bottom-2 right-8 text-xs text-oldLace">{`closing in ${timer}s`}</span>
</div>);
};
Why is the setInterval printing a stream of 0s in console and not updating the DOM?
You are loggin, comparing, and setting a stale value due to closures.
See more use cases in a related question.
useEffect(() => {
// timerRef from useRef
timerRef.current = setInterval(() => {
setTimer((prevTimer) => prevTimer + 1);
}, 1000);
}, []);
useEffect(() => {
console.log("timer", timer);
if (timer === 3) {
clearInterval(timerRef.current);
}
}, [timer]);
Check out the code for useInterval in react-use. Inspecting the different hooks in this package can greatly improve your hooks understanding.
import { useEffect, useRef } from 'react';
const useInterval = (callback: Function, delay?: number | null) => {
const savedCallback = useRef<Function>(() => {});
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
if (delay !== null) {
const interval = setInterval(() => savedCallback.current(), delay || 0);
return () => clearInterval(interval);
}
return undefined;
}, [delay]);
};
export default useInterval;
And the usage as described in the docs:
import * as React from 'react';
import {useInterval} from 'react-use';
const Demo = () => {
const [count, setCount] = React.useState(0);
const [delay, setDelay] = React.useState(1000);
const [isRunning, toggleIsRunning] = useBoolean(true);
useInterval(
() => {
setCount(count + 1);
},
isRunning ? delay : null
);
return (
<div>
<div>
delay: <input value={delay} onChange={event => setDelay(Number(event.target.value))} />
</div>
<h1>count: {count}</h1>
<div>
<button onClick={toggleIsRunning}>{isRunning ? 'stop' : 'start'}</button>
</div>
</div>
);
};
To start the interval on mount simply change the value of isRunning on mount:
useMount(()=>{
toggleIsRunning(true);
});
I was doing a project in React and what I wanted to do is to start the calculation of factorial of 1000 on start button and cancel the calculation on cancel button click. Thus, I used setInterval here is the whole code:
import React, { useState } from "react";
const Button = ({ title, countButton }) => {
const [result, setResult] = useState(0);
let interval;
const handleFactorial = (num) => {
let iteration = 1;
let value = 1;
interval = setInterval(function () {
value = value * iteration;
console.log(iteration++);
if (iteration === num) {
setResult(value);
console.log(result);
clearInterval(interval);
}
}, 0);
};
let cancelFactorial = () => {
clearInterval(interval);
};
return countButton ? (
<button onClick={() => handleFactorial(1000)}>{title}</button>
) : (
<button onClick={cancelFactorial}>{title}</button>
);
};
export default Button;
The problem is when I click on cancel button which is this one <button onClick={cancelFactorial}>{title}</button> but calculation keeps going. Thus I need your help
You should use a reference for that as if you log your interval value, you will notice that you re-assign its value on every render.
const Button = ({ title, countButton }) => {
const intervalRef = useRef();
const handleFactorial = (num) => {
intervalRef.current = setInterval(function () {...}, 0);
};
let cancelFactorial = () => {
clearInterval(intervalRef.current);
};
...
}
You need to use [useRef][1] to keep a reference to your interval.
// don't do that
// let interval;
// do this instead
const intervalRef = useRef();
interval.current = setInterval(function () { ... })
const cancelFactorial = () => {
clearInterval(interval.current);
};