State within useEffect not updating - javascript

I'm currently building a timer in ReactJS for practice. Currently I have two components, a Timer component which displays the time as well as sets an interval upon mounting. I also have an App component which keeps tracks of the main state, such as state for whether the timer is paused as well as the current value of the timer.
My goal is to make it so that when I click the pause button, the timer stops incrementing. Currently I've tried to achieve this by using:
if(!paused)
tick(time => time+1);
which in my mind, should only increment the time state when paused is false. However, when I update the paused state by clicking on my button, this paused state inside the setTimeout does not change. My guess that setTimeout is forming a closure over the paused state, so it's not updating when the state changes. I tried adding paused as a dependency to useEffect but this caused multiple timeouts to be queued whenever paused changed.
const {useState, useEffect} = React;
const Timer = ({time, paused, tick}) => {
useEffect(() => {
const timer = setInterval(() => {
if(!paused) // `paused` doesn't change?
tick(time => time+1);
}, 1000);
}, []);
return <p>{time}s</p>
}
const App = () => {
const [time, setTime] = useState(0);
const [paused, setPaused] = useState(false);
const togglePaused = () => setPaused(paused => !paused);
return (
<div>
<Timer paused={paused} time={time} tick={setTime} />
<button onClick={togglePaused}>{paused ? 'Play' : 'Pause'}</button>
</div>
);
}
ReactDOM.render(<App />, document.body);
<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>
So, my question is:
Why isn't my current code working (why is paused not updating within useEffect)?
Is there any way to make the above code work so that I can "pause" my interval which is set within the useEffect()?

To stop the timer return the function clearing the interval from useEffect().
React performs the cleanup when the component unmounts. However, as we learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time.
(source: Using the Effect Hook - Effects with Cleanup)
You should also pass the paused in dependencies array to stop useEffect creating new intervals on each re-render. If you add [paused] it'll only create new interval when paused change.
const {useState, useEffect} = React;
const Timer = ({time, paused, tick}) => {
useEffect(() => {
const timer = setInterval(() => {
if(!paused) // `paused` doesn't change?
tick(time => time+1);
}, 1000);
return () => clearInterval(timer);
}, [paused]);
return <p>{time}s</p>
}
const App = () => {
const [time, setTime] = useState(0);
const [paused, setPaused] = useState(false);
const togglePaused = () => setPaused(paused => !paused);
return (
<div>
<Timer paused={paused} time={time} tick={setTime} />
<button onClick={togglePaused}>{paused ? 'Play' : 'Pause'}</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<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>
<div id="root"></div>

The setInterval captures the paused value the first time, you'll need to remove the interval, recreate it everytime paused is changed.
You can check this article for more info: https://overreacted.io/making-setinterval-declarative-with-react-hooks

You can use Dan's useInterval hook. I can't explain it better than him why you should use this.
The hook looks like this.
function useInterval(callback, delay) {
const savedCallback = React.useRef();
// Remember the latest callback.
React.useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
React.useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
Then use it in your Timer component like this.
const Timer = ({ time, paused, tick }) => {
useInterval(() => {
if (!paused) tick(time => time + 1);
}, 1000);
return <p>{time}s</p>;
};
I believe the difference is that useInterval hook is aware of its dependencies (paused) while setInterval isn't.

Related

useEffect calling api's couple of times reactjs

I have this useEffect function in react component. I am calling api videoGridState here.
Now what is happening here it is calling my api 2 times one at intitial page reaload and second one when count is changing. I want it to be called single time when page reloads. But also when streamSearchText changes
const [count, setCount] = useState(0);
const [streamSearchText, setStreamSearchText] = useState("");
useEffect(() => {
videoGridState();
}, [count]);
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
setCount(count + 1);
}, 1000);
return () => clearTimeout(delayDebounceFn);
}, [streamSearchText]);
How can I do that?
The main issue is that you have two useEffect calls, and so they're each handled, and the second triggers the first (after a delay), resulting in the duplication.
As I understand it, your goal is:
Run videoGridState immediately on mount, and
Run it again after a delay of 1000ms whenever streamSearchText changes
That turns out to be surprisingly awkward to do. I'd probably end up using a ref for it:
const firstRef = useRef(true);
const [streamSearchText, setStreamSearchText] = useState("");
useEffect(() => {
if (firstRef.current) {
// Mount
videoGridState();
firstRef.current = false;
} else {
// `streamSearchText` change
const timer = setTimeout(() => {
videoGridState();
}, 1000);
return () => clearTimeout(timer);
}
}, [streamSearchText]);
Live Example:
const { useState, useRef, useEffect } = React;
function videoGridState() {
console.log("videoGridState ran");
}
const Example = () => {
const firstRef = useRef(true);
const [streamSearchText, setStreamSearchText] = useState("");
useEffect(() => {
if (firstRef.current) {
// Mount
videoGridState();
firstRef.current = false;
} else {
// `streamSearchText` change
const timer = setTimeout(() => {
videoGridState();
}, 1000);
return () => clearTimeout(timer);
}
}, [streamSearchText]);
return <div>
<label>
Search text:{" "}
<input
type="text"
value={streamSearchText}
onChange={(e) => setStreamSearchText(e.currentTarget.value)}
/>
</label>
</div>;
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
You could also do the query immediately when streamSearchText is "", but that would happen every time streamSearchText was "", not just on mount. That may be good enough, depending on how rigorous you need to be.
Additionally, though, if you're still seeing something happen "on mount" twice, you may be running a development copy of the libraries with React.StrictMode around your app (the default in many scaffolding systems). See this question's answers for details on how React.StrictMode may mount your component more than once and throw in other seeming surprises.
Your following useEffect() function makes this behaviour to happen:
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
setCount(count + 1);
}, 1000);
return () => clearTimeout(delayDebounceFn);
}, [streamSearchText]);
Since it runs initially but called setCount() which updates the state, and forces a re-render of the component which in turn runs the first useEffect() since that has [count] in the dependency array.
And hence the cycle continues for the [count]
const Example = () => {
const { useState, useRef, useEffect } = React;
// Any async function or function that returns a promise
function myDownloadAsyncFunction(data) {
console.log("222222222222222222222")
return new Promise((resolve) => setTimeout(resolve, 1000));
}
function DownloadButton() {
const [queue, setQueue] = useState(Promise.resolve());
onClickDownload = () => {
setQueue(queue
.then(() => myDownloadAsyncFunction('My data'))
.catch((err) => {console.error(err)})
)
}
return (
<button onClick={onClickDownload()}>Download</button>
);
}
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

useEffect with react-hooks/exhaustive-deps where callbacks depend on state

I've got a hooks problem I've been unable to find an answer for. The closest article being this other post.
Essentially I want to have a function that is invoked once per component lifecycle with hooks. I'd normally use useEffect as such:
useEffect(() => {
doSomethingOnce()
setInterval(doSomethingOften, 1000)
}, [])
I'm trying not to disable eslint rules and get a react-hooks/exhaustive-deps warning.
Changing this code to the following creates an issue of my functions being invoked every time the component is rendered... I don't want this.
useEffect(() => {
doSomethingOnce()
setInterval(doSomethingOften, 1000)
}, [doSomethingOnce])
The suggested solution is to wrap my functions in useCallback but what I can't find the answer to is what if doSomethingOnce depends on the state.
Below is the minimal code example of what I am trying to achieve:
import "./styles.css";
import { useEffect, useState, useCallback } from "react";
export default function App() {
const [count, setCount] = useState(0);
const getRandomNumber = useCallback(
() => Math.floor(Math.random() * 1000),
[]
);
const startCounterWithARandomNumber = useCallback(() => {
setCount(getRandomNumber());
}, [setCount, getRandomNumber]);
const incrementCounter = useCallback(() => {
setCount(count + 1);
}, [count, setCount]);
useEffect(() => {
startCounterWithARandomNumber();
setInterval(incrementCounter, 1000);
}, [startCounterWithARandomNumber, incrementCounter]);
return (
<div className="App">
<h1>Counter {count}</h1>
</div>
);
}
As you can see from this demo since incrementCounter depends on count it gets recreated. Which in turn re-invokes my useEffect callback that I only wanted to be called the once. The result is that the startCounterWithARandomNumber and incrementCounter get called many more times than I would expect.
Any help would be much appreciated.
Update
I should have pointed out, this is a minimal example of a real-use case where the events are aysncronous.
In my real code the context is a live transcription app. I initially make a fetch call to GET /api/all.json to get the entire transcription then poll GET /api/latest.json every second merging the very latest speech to text from with the current state. I tried to emulate this in my minimal example with a seed to start followed by a polled method call that has a dependency on the current state.
I think you have overcomplicated your code considerably.
Solution
If you want the startCounterWithARandomNumber function to run once to set initial state, then just use a state initialization function.
const getRandomNumber = () => Math.floor(Math.random() * 1000);
const [count, setCount] = useState(getRandomNumber);
As for the effect setting up the interval, you will also want to only run this once when mounting. Move the interval callback that "ticks" and increments the count state into the effect callback so it is no longer a dependency. Use a functional state update to correctly update from the previous state and not the initial state. Don't forget to return a cleanup function from the useEffect hook to clear any running interval timers.
useEffect(() => {
const incrementCounter = () => setCount((c) => c + 1);
const timer = setInterval(incrementCounter, 1000);
return () => clearInterval(timer);
}, []);
Demo
Full code:
import { useEffect, useState } from "react";
export default function App() {
const getRandomNumber = () => Math.floor(Math.random() * 1000);
const [count, setCount] = useState(getRandomNumber);
useEffect(() => {
const incrementCounter = () => setCount((c) => c + 1);
const timer = setInterval(incrementCounter, 1000);
return () => clearInterval(timer);
}, []);
return (
<div className="App">
<h1>Counter {count}</h1>
</div>
);
}
Update
I guess it's still a bit unclear what your real code and use case is doing. It seems you've written your doSomethingOnce and doSomethingOften functions in such a way so as to still have some outer dependency that when used inside an useEffect hook is flagged by the linter.
Based on your counting example here an example that doesn't warn about dependencies.
const getRandomNumber = () => Math.floor(Math.random() * 1000);
const fetch = () =>
new Promise((resolve) => {
setTimeout(() => {
if (Math.random() < 0.1) { // 10% to return new value
resolve(getRandomNumber());
}
}, 3000);
});
function App() {
const [count, setCount] = React.useState(0);
const doSomethingOnce = () => {
console.log('doSomethingOnce');
fetch().then((val) => setCount(val));
};
const doSomethingOften = () => {
console.log('doSomethingOften');
fetch().then((val) => {
console.log('new value, update state')
setCount(val);
});
};
React.useEffect(() => {
doSomethingOnce();
const timer = setInterval(doSomethingOften, 1000);
return () => clearInterval(timer);
}, []);
// This effect is only to update state independently of any state updates from "polling"
React.useEffect(() => {
const tick = () => setCount((c) => c + 1);
const timer = setInterval(tick, 100);
return () => clearInterval(timer);
}, []);
return (
<div className="App">
<h1>Counter {count}</h1>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
<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>
<div id="root"></div>
This is sort of a contrived and "tuned" solution though since the "fetch" only resolves when it's returning a new value. In reality if you are likely polling and completely replacing a chunk of state that the component isn't regularly modifying (at least I hope it isn't as this makes merging/synchronizing more difficult). I can improve this answer if there were a better/clearer view of what your code is actually doing.
I dont know the usecase you want to solve so keeping minimal code changes to your code.
A very simple solution would be to increment counter as
const incrementCounter = useCallback(() => {
setCount(prevCount => prevCount+1);
}, [setCount]);
This way the increment counter does not rely on count.
Forked from your sandbox.
https://codesandbox.io/s/fragrant-architecture-t58bs

React interval using old state inside of useEffect

I ran into a situation where I set an interval timer from inside useEffect. I can access component variables and state inside the useEffect, and the interval timer runs as expected. However, the timer callback doesn't have access to the component variables / state. Normally, I would expect this to be an issue with "this". However, I do not believe "this" is the the case here. No puns were intended. I have included a simple example below:
import React, { useEffect, useState } from 'react';
const App = () => {
const [count, setCount] = useState(0);
const [intervalSet, setIntervalSet] = useState(false);
useEffect(() => {
if (!intervalSet) {
setInterval(() => {
console.log(`count=${count}`);
setCount(count + 1);
}, 1000);
setIntervalSet(true);
}
}, [count, intervalSet]);
return <div></div>;
};
export default App;
The console outputs only count=0 each second. I know that there's a way to pass a function to the setCount which updates current state and that works in this trivial example. However, that was not the point I was trying to make. The real code is much more complex than what I showed here. My real code looks at current state objects that are being managed by async thunk actions. Also, I am aware that I didn't include the cleanup function for when the component dismounts. I didn't need that for this simple example.
The first time you run the useEffect the intervalSet variable is set to true and your interval function is created using the current value (0).
On subsequent runs of the useEffect it does not recreate the interval due to the intervalSet check and continues to run the existing interval where count is the original value (0).
You are making this more complicated than it needs to be.
The useState set function can take a function which is passed the current value of the state and returns the new value, i.e. setCount(currentValue => newValue);
An interval should always be cleared when the component is unmounted otherwise you will get issues when it attempts to set the state and the state no longer exists.
import React, { useEffect, useState } from 'react';
const App = () => {
// State to hold count.
const [count, setCount] = useState(0);
// Use effect to create and clean up the interval
// (should only run once with current dependencies)
useEffect(() => {
// Create interval get the interval ID so it can be cleared later.
const intervalId = setInterval(() => {
// use the function based set state to avoid needing count as a dependency in the useEffect.
// this stops the need to code logic around stoping and recreating the interval.
setCount(currentCount => {
console.log(`count=${currentCount}`);
return currentCount + 1;
});
}, 1000);
// Create function to clean up the interval when the component unmounts.
return () => {
if (intervalId) {
clearInterval(intervalId);
}
}
}, [setCount]);
return <div></div>;
};
export default App;
You can run the code and see this working below.
const App = () => {
// State to hold count.
const [count, setCount] = React.useState(0);
// Use effect to create and clean up the interval
// (should only run once with current dependencies)
React.useEffect(() => {
// Create interval get the interval ID so it can be cleared later.
const intervalId = setInterval(() => {
// use the function based set state to avoid needing count as a dependency in the useEffect.
// this stops the need to code logic around stoping and recreating the interval.
setCount(currentCount => {
console.log(`count=${currentCount}`);
return currentCount + 1;
});
}, 1000);
// Create function to clean up the interval when the component unmounts.
return () => {
if (intervalId) {
clearInterval(intervalId);
}
}
}, [setCount]);
return <div></div>;
};
ReactDOM.render(<App />, document.getElementById('app'))
<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="app"></div>
If you need a more complex implementation as mention in your comment on another answer, you should try using a ref perhaps. For example, this is a custom interval hook I use in my projects. You can see there is an effect that updates callback if it changes.
This ensures you always have the most recent state values and you don't need to use the custom updater function syntax like setCount(count => count + 1).
const useInterval = (callback, delay) => {
const savedCallback = useRef()
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
if (delay !== null) {
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}
}, [delay])
}
// Usage
const App = () => {
useInterval(() => {
// do something every second
}, 1000)
return (...)
}
This is a very flexible option you could use. However, this hook assumes you want to start your interval when the component mounts. Your code example leads me to believe you want this to start based on the state change of the intervalSet boolean. You could update the custom interval hook, or implement this in your component.
It would look like this in your example:
const useInterval = (callback, delay, initialStart = true) => {
const [start, setStart] = React.useState(initialStart)
const savedCallback = React.useRef()
React.useEffect(() => {
savedCallback.current = callback
}, [callback])
React.useEffect(() => {
if (start && delay !== null) {
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}
}, [delay, start])
// this function ensures our state is read-only
const startInterval = () => {
setStart(true)
}
return [start, startInterval]
}
const App = () => {
const [countOne, setCountOne] = React.useState(0);
const [countTwo, setCountTwo] = React.useState(0);
const incrementCountOne = () => {
setCountOne(countOne + 1)
}
const incrementCountTwo = () => {
setCountTwo(countTwo + 1)
}
// Starts on component mount by default
useInterval(incrementCountOne, 1000)
// Starts when you call `startIntervalTwo(true)`
const [intervalTwoStarted, startIntervalTwo] = useInterval(incrementCountTwo, 1000, false)
return (
<div>
<p>started: {countOne}</p>
<p>{intervalTwoStarted ? 'started' : <button onClick={startIntervalTwo}>start</button>}: {countTwo}</p>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('app'))
<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="app"></div>
The problem is the interval is created only once and keeps pointing to the same state value. What I would suggest - move firing the interval to separate useEffect, so it starts when the component mounts. Store interval in a variable so you are able to restart it or clear. Lastly - clear it with every unmount.
const App = () => {
const [count, setCount] = React.useState(0);
const [intervalSet, setIntervalSet] = React.useState(false);
React.useEffect(() => {
setIntervalSet(true);
}, []);
React.useEffect(() => {
const interval = intervalSet ? setInterval(() => {
setCount((c) => {
console.log(c);
return c + 1;
});
}, 1000) : null;
return () => clearInterval(interval);
}, [intervalSet]);
return null;
};
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>

Trying to change a state in React Hooks with Interval

I have a problem trying to change a state in react using setInterval.
Why does the function "alert" always show a "null" message?
import React, { useEffect, useState } from 'react';
const Playlist = (props) => {
const [test, setTest] = useState("");
useEffect(() => {
setInterval(load, 3000)
}, []);
const load = async () => {
alert(test)
setTest("TEST");
}
return (
<div>
{test}
</div>
)
}
export default Playlist
Issue :
is callback passed into setInterval's closure and it accesses the test variable in the first render.
Solution :
You can clearInterval and setInterval as soon as there is any changes in your state
const { useState , useEffect , useCallback } = React;
const App = () => {
const [test, setTest] = useState("");
useEffect(() => {
const intrvl = setInterval(load, 3000);
return () => clearInterval(intrvl);
}, [test]);
const load = () => {
alert(test)
setTest("TEST");
}
return (
<div>
{test}
</div>
)
}
ReactDOM.render(<App />, document.getElementById('react-root'));
<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>
<div id="react-root"></div>
The problem is that useEffect captures the test from the first render which is equal to empty string. We never re-apply the effect so the closure in setInterval always references the test from the first render.
NOTE: setInterval does not describe a process in time — once you set the interval, you can’t change anything about it except clearing it.
When we pass an empty [] array of dependencies to useEffect, it only runs the useEffect on mount and cleanup on unmount of the component.
One way is to add test to the array of dependencies so anytime there is an update on test, the useEffect will be called.
useEffect(() => {
const intervalId = setInterval(load, 3000);
return () => clearInterval(intervalId);
}, [test]);
But since we are using the test inside useEffect, we can just remove the empty [] array of dependencies so the useEffect will be called every time we are setting the state (component is re-rendered) and the new setInterval will be created with accessing to the latest test state like below
useEffect(() => {
const intervalId = setInterval(load, 3000);
return () => clearInterval(intervalId);
});
Another possible way is to update the state inside the interval's closure like
useEffect(() => {
const id = setInterval(() => {
setTest("TEST");
}, 3000);
return () => clearInterval(id);
});
const App = () => {
const [test, setTest] = useState("");
useEffect(() => {
const intervalId = setInterval(load, 3000);
return () => clearInterval(intervalId);
});
const load = () => {
alert(test)
setTest("TEST");
}
return (
<div>
{test}
</div>
)
}
Example of interval with setting count so you can see the state change

Use React hook to implement a self-increment counter [duplicate]

This question already has answers here:
State not updating when using React state hook within setInterval
(14 answers)
Closed 4 years ago.
The code is here: https://codesandbox.io/s/nw4jym4n0
export default ({ name }: Props) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCounter(counter + 1);
}, 1000);
return () => {
clearInterval(interval);
};
});
return <h1>{counter}</h1>;
};
The problem is each setCounter trigger re-rendering so the interval got reset and re-created. This might looks fine since the state(counter) keeps incrementing, however it could freeze when combining with other hooks.
What's the correct way to do this? In class component it's simple with a instance variable holding the interval.
You could give an empty array as second argument to useEffect so that the function is only run once after the initial render. Because of how closures work, this will make the counter variable always reference the initial value. You could then use the function version of setCounter instead to always get the correct value.
Example
const { useState, useEffect } = React;
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCounter(counter => counter + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return <h1>{counter}</h1>;
};
ReactDOM.render(
<App />,
document.getElementById('root')
);
<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="root"></div>
A more versatile approach would be to create a new custom hook that stores the function in a ref and only creates a new interval if the delay should change, like Dan Abramov does in his great blog post "Making setInterval Declarative with React Hooks".
Example
const { useState, useEffect, useRef } = React;
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
let id = setInterval(() => {
savedCallback.current();
}, delay);
return () => clearInterval(id);
}, [delay]);
}
function App() {
const [counter, setCounter] = useState(0);
useInterval(() => {
setCounter(counter + 1);
}, 1000);
return <h1>{counter}</h1>;
};
ReactDOM.render(
<App />,
document.getElementById('root')
);
<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="root"></div>
As another answer by #YangshunTay already shows, it's possible to make it useEffect callback run only once and work similarly to componentDidMount. In this case it's necessary to use state updater function due to the limitations imposed by function scopes, otherwise updated counter won't be available inside setInterval callback.
The alternative is to make useEffect callback run on each counter update. In this case setInterval should be replaced with setTimeout, and updates should be limited to counter updates:
export default ({ name }: Props) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const timeout = setTimeout(() => {
setCounter(counter + 1);
}, 1000);
return () => {
clearTimeout(timeout);
};
}, [counter]);
return <h1>{counter}</h1>;
};
The correct way to do this would be to run the effect only once. Since you only need to run the effect once because during mounting, you can pass in an empty array as a second argument to achieve.
However, you will need to change setCounter to use the previous value of counter. The reason is because the callback passed into setInterval's closure only accesses the counter variable in the first render, it doesn't have access to the new counter value in the subsequent render because the useEffect() is not invoked the second time; counter always has the value of 0 within the setInterval callback.
Like the setState you are familiar with, state hooks have two forms: one where it takes in the updated state, and the callback form which the current state is passed in. You should use the second form and read the latest state value within the setState callback to ensure that you have the latest state value before incrementing it.
function Counter() {
const [counter, setCounter] = React.useState(0);
React.useEffect(() => {
const timer = setInterval(() => {
setCounter(prevCount => prevCount + 1); // <-- Change this line!
}, 1000);
return () => {
clearInterval(timer);
};
}, []); // Pass in empty array to run effect only once!
return (
<div>Count: {counter}</div>
);
}
ReactDOM.render(<Counter />, 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>

Categories

Resources