I'm trying to create a simple recursive function that adds random numbers to an array until the sum of that array is 10.
function App() {
const [array, setArray] = useState([1, 2, 3]);
const [sum, setSum] = useState(0);
const getRandNum = () => {
const num = Math.floor(Math.random() * 5);
setArray((prev) => [...prev, num]);
};
useEffect(() => {
setSum(() => {
return array.reduce(
(acumulator, currentValue) => acumulator currentValue,
0
);
});
}, [array]);
const func = () => {
if (sum >= 10) return;
setTimeout(() => {
getRandNum();
func();
}, 500);
};
return (
<>
<div className="App">
{array.map((num) => (
<p>{num}</p>
))}
</div>
<button onClick={() => func()}>click</button>
<p>sum: {sum}</p>
</>
);
}
This recursive function func()
does not stop calling itself. It keeps adding numbers even though sum
is greater than 10.
I know what's the problem but I don't know how to fix it. If I add console.log
into func
like this:
const func = () => {
if (sum >= 10) return;
setTimeout(() => {
getRandNum();
console.log(array, sum);
func();
}, 500);
};
It always logs the same numbers: [1,2,3] 6
. So even though array and sum are changing, function keeps calling itself with same values and it does not stop.
CodePudding user response:
Your function does not work because it will not take into consideration any changes that happen to the state. Same thing for setTimeout()
.
This is because when setTimeout()
or the recursive func()
are scheduled, they are using the values of sum
and array
at the time they were scheduled.
If you want to store a value that is capable of being updated during the execution, you should use the useRef
hook. More about it here.
PS: When the value of useRef
changes, it does not cause a re-render like useState
. It also won't trigger useEffect
if put in its dependency array.
First, start by declaring two refs like so:
// Your states
const [array, setArray] = useState([1, 2, 3]);
const [sum, setSum] = useState(0);
// The refs
const arrayRef = useRef([1, 2, 3]);
const sumRef = useRef(0);
Then, whenever array and sum changes, you should update the values of these refs:
useEffect(() => {
arrayRef.current = [...array];
setSum(() => {
return array.reduce(
(acumulator, currentValue) => acumulator currentValue,
0
);
});
}, [array]);
useEffect(() => {
sumRef.current = sum;
}, [sum]);
Finally, you make the recursive function work with the refs instead of the states:
const func = () => {
if (sumRef.current >= 10) return;
setTimeout(() => {
getRandNum();
console.log(arrayRef.current, sumRef.current);
func();
}, 500);
};
You can see the code in action in this codesandbox
Hope this helps :)
CodePudding user response:
You need to be able to reference the new values that are put in state. Doing just getRandNum();
means that you don't have access to them elsewhere in that block; you might be triggering a re-render, but that func
function still closes over the value array
had at the moment func
was first invoked.
A good way to do this would be to add another state - a boolean flag that indicates whether you're in the process of adding values or not. Check that state on every render and perform the psuedo-recursive asynchronous update if needed.
Also, the sum
state is superfluous, because it depends entirely on another state, array
. (Don't duplicate state!) Use useMemo
instead to indicate the dependency and calculate it synchronously.
const { useState, useEffect, useMemo } = React;
function App() {
const [array, setArray] = useState([1, 2, 3]);
const sum = useMemo(() => array.reduce((a, b) => a b, 0), [array]);
const [funcRunning, setFuncRunning] = useState(false);
const getRandNum = () => {
const num = Math.floor(Math.random() * 5);
setArray((prev) => [...prev, num]);
};
// Run on every render:
useEffect(() => {
if (funcRunning) {
if (sum >= 10) {
setFuncRunning(false);
return;
}
const timeoutId = setTimeout(getRandNum, 500);
return () => clearTimeout(timeoutId);
}
});
return (
<div>
<div className="App">
{array.map((num, i) => (
<p key={i}>{num}</p>
))}
</div>
<button onClick={() => setFuncRunning(true)}>click</button>
<p>sum: {sum}</p>
</div>
);
}
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div class='react'></div>