By not going into the actual usage, I have created a simple example to explain what I want to do.
I have a state object {num:0}
and I want to update the num after every second for 10 seconds and according to that, I created a class component that is working perfectly fine.
class App extends React.Component {
constructor() {
super();
this.state = {
num: 0
};
}
componentDidMount = () => {
for (let i = 0; i < 10; i++) {
setTimeout(() => this.setState({ num: this.state.num + 1 }), i * 1000);
}
};
render() {
return (
<>
<p>hello</p>
<p>{this.state.num}</p>
</>
);
}
}
Now I want to replicate the same functionality in the functional component but I am unable to. I tried as shown below:
const App = () => {
const [state, setState] = React.useState({ num: 0 });
React.useEffect(() => {
for (let i = 0; i < 10; i++) {
setTimeout(() => setState({ num: state.num + 1 }), i * 1000);
}
}, []);
return (
<>
<p>hello</p>
<p>{state.num}</p>
</>
);
};
Can anyone please help me with what I am doing wrong here?
All of your timeouts do run, but because you set them all on the first render you've created a closure around the initial state.num
value, so when each one fires it sets the new state value to 0 + 1
and nothing changes.
The duplicate noted in the comment covers the details, but here's a quick working snippet using a ref
as a counter to stop at after 10 iterations and cleaning up the timer in the return of the useEffect.
const App = () => {
const [state, setState] = React.useState({ num: 0 });
const counter = React.useRef(0);
React.useEffect(() => {
if (counter.current < 10) {
counter.current += 1;
const timer = setTimeout(() => setState({ num: state.num + 1 }), 1000);
return () => clearTimeout(timer);
}
}, [state]);
return (
<div>
<p>hello</p>
<p>{state.num}</p>
</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>
To make your code work as it is, setting all of them at once you can pass a callback to the setState()
call in order to avoid creating a closure, but you give up the granular control allowed by setting new timeouts on each render.
const App = () => {
const [state, setState] = React.useState({ num: 0 });
React.useEffect(() => {
for (let i = 0; i < 10; i++) {
setTimeout(() => setState(prevState => ({ ...prevState, num: prevState.num + 1 })), i * 1000);
}
}, []);
return (
<div>
<p>hello</p>
<p>{state.num}</p>
</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>