How to use setInterval with react useEffect hook correctly? - javascript

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])
...
}

Related

Unable to get updated props in setInterval React functional component

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

Component flickers after updated data found in React Native

I have a React Native component where i'm calling for data in useEffect and rendering data accordingly. But i need to call for data after every 2 seconds and i'm doing that. But the whole component flickers after every two seconds. How can i stop that? Here's my component right now:
const Climate = () => {
const rooms: ControllerRoom<ClimateRoomItemType>[] =
useSelector(climateSelector);
const displayRoomList = useSelector(displayRoomListSelector);
//State
const [active, setActive] = useState<number>(-1);
const [count, setCount] = useState<number>(0);
const [isFetching, setIsFetching] = useState<boolean>(true);
//Handlers
const fetchClimate = useCallback(
() => dispatch(desktopAction.fetchClimate()),
[dispatch],
);
useLayoutEffect(() => {
fetchClimate();
fetchData();
}, []);
const fetchData = useCallback(
() => {
const interval = setInterval(() => {
fetchClimate();
}, 2000);
return () => {
fetchClimate();
clearInterval(interval);
};
},[]
);
useEffect(() => {
if (active < 0 && rooms.length > 0) {
setActive(newlist[0][0].id);
setIsFetching(false);
}
}, [rooms, isFetching]);
return(
<ControllerContainer>
{!displayRoomList ? (
<RoomsClimate
key={Math. floor(Math. random() * 100)}
rooms={rooms}
displayRoomList={displayRoomList}
/>
) : null}
</ControllerContainer>
)
}
export default memo(Climate);
How can i stop flickering of component and still ask for data every 2 seconds?

SetInterval on mount for a set duration

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);
});

Unable to access state inside interval

I am unable to access the state within an interval. Here I want to access counter inside interval when the counter gets equal to 10 I want to stop it.
Note: Here I don't want to put an interval inside useEffect because I need to start the interval in a specific time, by handling an event.
export default props => {
const [counter, setCounter] = useState(0);
const startInterval = () => {
const timeout = setInterval(() => {
setCounter(counter + 1);
console.log("counter: ", counter); // alway return 0
if(counter === 10) clearInterval(timeout);
}, 1000);
};
}
As I am seeing here even your setCounter(counter+1) wont update, because of lexical scope. So you have to change it like this:
setCounter(counter => counter + 1);
Also because of lexical scope you wont access counter to check condition, so you have to make a variable and update that inside functional component by asigning counter to it, then check it with if condition.
Complete Code code:
let myCounter = 0;
let timeout = null;
export default CounterApp = props => {
const [counter, setCounter] = useState(0);
// Also don't forget this
useEffect(()=> {
return ()=> clearInterval(timeout);
}, []);
myCounter = counter;
const startInterval = () => {
timeout = setInterval(() => {
setCounter(counter => counter + 1);
console.log("counter: ", myCounter); // counter always return 0 but myCounter the updated value
if(myCounter === 10) clearInterval(timeout);
}, 1000);
};
}
I came across this exact problem not too long ago. Hooks don't work exactly as you'd expect in relation to setInterval. I found the solution on Dan Abramov's blog: You can useRef to combine multiple useEffects. Through his useInterval implementation you can also start and stop the interval by setting the timer to null
import React, { useState, useEffect, useRef } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [time, setTime] = useState(null); // timer doesn't run initially
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, time);
return <>
<h1>{count}</h1>
<div onClick={() => setTime(1000)}>Start</div>
<div onClick={() => setTime(null)}>Stop</div>
</>;
}
import React, { useState, useEffect, useRef } from 'react';
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}

Unable to access counter after gets updated

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).

Categories

Resources