♾️🔁❌How to Avoid Infinite Loops when Updating State in React

I am building a Mortgage calculator, where a user can enter either Down-Payment amount or percentage and either one will update the other. After struggling with implementation for some time I eventually figured out how to get this working right. In this post, I'll explain the problem and my solution to hopefully save you time and if you have to implement something similar.

The Problem: State Updates Triggering State Updates
I want to update the other field whenever one of them changes. For example, when the user changes the down payment amount, I want to update the down payment percentage to reflect the new amount, and vice versa.
Here's a simplified sample of the code that I implemented for this functionality:
const [input, setInput] = useState({
down: 0,
downPercent: 0,
propertyPrice: 0,
});
// changing down-payment amount calculates percentage
useEffect(() => {
if (input.propertyPrice > 0) {
setInput({
...input,
downPercent:
Math.round((input.down / input.propertyPrice) * 100 * 10)/ 10,
});
}
}, [input.down, input.propertyPrice]);
//^ dependencies that this useEffect is watching for
// changing percentage recalculates down-payment amount
useEffect(() => {
if (input.propertyPrice > 0) {
setInput({
...input,
down: Math.round((input.downPercent / 100) * input.propertyPrice),
});
}
}, [input.downPercent, input.propertyPrice]);
//^ dependencies that this useEffect is watching for
return (
<div>
<label>
Down Payment Amount:
<input type="number" value={input.down} onChange={(e) => setInput({ ...input, down: e.target.value })} />
</label>
<label>
Down Payment Percentage:
<input type="number" value={input.downPercent} onChange={(e) => setInput({ ...input, downPercent: e.target.value })} />
</label>
</div>
);
The useEffect hooks update the input state whenever the down or downPercent fields change. However, this code has a problem: it creates an infinite loop of state updates. Here's what happens:
The user changes the
downfield by entering a new down payment amount.The
useEffecthook that recalculates thedownPercentfield is triggered becauseinput.downhas changed.The
useEffecthook updates theinputstate to set the newdownPercentvalue.Setting the
inputstate triggers a re-render of the component.The
useEffecthook that recalculates thedownfield is triggered becauseinput.downPercenthas changed.The
useEffecthook updates theinputstate to set the newdownvalue.Setting the
inputstate triggers a re-render of the component.Go to step 2.
This creates an infinite loop of updates that will continue until the application crashes or becomes unresponsive (in my case unresponsiveness was occurring in the form of React skipping a step and updating the current input when another entry is made, and it feels like its 1 step behind the input).
The Solution: Trigger State Updates Outside of useEffect
The solution to this problem is to trigger state updates outside of the useEffect hooks. This can be done by setting the editing state when the user changes a field, and then checking the editing state in the useEffect hooks to determine whether or not to update the other field.
Here's the modified code that implements this solution:
const [input, setInput] = useState({
down: 0,
downPercent: 0,
propertyPrice: 0,
});
//state to control which input is being edited
const [editing, setEditing] = useState('');
//changing down-payment amount calculates percentage
useEffect(() => {
if (input.propertyPrice > 0 && editing === 'amount') {
setInput({
...input,
downPercent:
Math.round((input.down / input.propertyPrice) * 100 * 10) / 10,
});
}
}, [input.down, input.propertyPrice]);
//^ dependencies that this useEffect is watching for
//changing percentage recalculates down-payment amount
useEffect(() => {
if (input.propertyPrice > 0 && editing === 'percent') {
setInput({
...input,
down: Math.round((input.downPercent / 100) * input.propertyPrice),
});
}
}, [input.downPercent, input.propertyPrice]);
//^ dependencies that this useEffect is watching for
return (
<div>
<label>
Down Payment Amount:
<input type="number" value={input.down} onChange={(e) => { setInput({ ...input, down: e.target.value }); setEditing('amount'); }} />
</label>
<label>
Down Payment Percentage:
<input type="number" value={input.downPercent} onChange={(e) => { setInput({ ...input, downPercent: e.target.value }); setEditing('percent'); }} />
</label>
</div>
);
The key changes are:
I added a new state variable,
editing, which tracks which field the user is currently editing.I modified the
onChangehandlers for theinputelements to set theeditingstate to'amount'or'percent'depending on which field was changed.I added a check for the
editingstate in theuseEffecthooks to determine whether or not to update the other field.
With these changes, the infinite loop of state updates is avoided, and the component behaves as expected.
I hope this post helps you if you run into a similar issue. Happy coding!



