I have the following situation and don't know what's the best way to approach it. I want a component with two states, playing
and items
, when playing
is set to true
, it should add a new item to items
every second, where the new item depends on the content in items
so far. So my naive approach would be the following:
function App() {
const [playing, setPlaying] = useState(false);
const [items, setItems] = useState([]);
const addItem = useCallback(
function () {
/* adding new item */
},
[items]
);
useEffect(
function () {
let timeout;
playing &&
(function loop() {
timeout = window.setTimeout(loop, 1000);
addItem();
})();
return function () {
window.clearTimeout(timeout);
};
},
[addItem, playing]
);
/* render the items */
}
(I could use setInterval
here, but I want to add another state later on, to change the interval while the loop is running, for this setTimeout
works better.)
The problem here is that the effect depends on addItem
and addItem
depends on items
, so as soon as playing
switches to true, the effect will be caught in an infinite loop (adding a new item, then restarting itself immediately because items
has changed). What's the best way to avoid this?
One possibility would be using a ref pointing to items
, then have an effect only updating the ref whenever items
changes, and using the ref inside addItem
, but that doesn't seem like the React way of thinking.
Another possibility is to not use items
in addItem
but only setItems
and using a callback to get access to the current items
value. But this method fails when addItem
manipulates more than a single state (a situation I've encountered before).
Implements functional approach to setting state, while defining the function to invoke the same within useState to remove dependency. ultimately allows setInterval to be used which feels more natural for this case.
import * as React from "react";
import { useState, useEffect, useCallback } from "react";
import { render } from "react-dom";
function App() {
const [playing, setPlaying] = useState(true);
const [items, setItems] = useState([1]);
useEffect(
function () {
const addItem = () => (
setItems((arr) => [...arr, arr[arr.length - 1] + 1])
);
setTimeout(() => setPlaying(false), 10000)
let interval: any;
if (playing) {
(function() {
interval = window.setInterval(addItem, 1000);
})();
}
return function () {
clearInterval(interval);
};
},
[playing]
);
return (
<div>
{items.map((item) => (<div key={item}>{item}</div>))}
</div>
)
}
render(<App />, document.getElementById("root"));