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

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

·

4 min read

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:

  1. The user changes the down field by entering a new down payment amount.

  2. The useEffect hook that recalculates the downPercent field is triggered because input.down has changed.

  3. The useEffect hook updates the input state to set the new downPercent value.

  4. Setting the input state triggers a re-render of the component.

  5. The useEffect hook that recalculates the down field is triggered because input.downPercent has changed.

  6. The useEffect hook updates the input state to set the new down value.

  7. Setting the input state triggers a re-render of the component.

  8. 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 onChange handlers for the input elements to set the editing state to 'amount' or 'percent' depending on which field was changed.

  • I added a check for the editing state in the useEffect hooks 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!