How do I prevent this infinite loop triggered by setting state via onChange in a controlled select element?

In order to determine how to allocate the debt in my bill splitting exercise, I have decided to use a select element with an option of “you” or “peer”. A state object contains the state that determines the calculation (based off the select value)

My issue is that when I set the select value to the state needed and then use the setter function in the callback to onChange to set the state to the current select value, I get an infinite loop. When I console.log the target value I noticed that if I put a value for my select element then the screen never changes to the newly selected option. If I use a defaultValue then it does, but I need it to be a controlled input.

I wrote an explicit comment around my problem area (towards the bottom of the component, inside the return), but will include the entire App component in case the context is needed

import "./App.css";
// components
import { PeerCard } from "./components/PeerCard/PeerCard";
import { BillRow } from "./components/BillRow/BillRow";
import { AddPeer } from "./components/AddPeer/AddPeer";
import { useEffect, useState } from "react";

function App() {
  const staticPeers = [
    {
      name: "Clark",
      // id: 0,
      money: -7,
      selected: false,
      profilePic:
        "https://kevindayphotography.com/wp-content/uploads/2020/12/headshot-for-business-man-1200x675.jpg",
    },
    {
      name: "Sarah",
      // id: 1,
      money: 27,
      selected: true,
      profilePic:
        "https://www.cityheadshots.com/uploads/5/1/2/1/5121840/editor/lowres-mjb-5074_3.jpg?1574008349",
    },
    {
      name: "Anthony",
      // id: 2,
      money: 0,
      selected: false,
      profilePic:
        "https://images.squarespace-cdn.com/content/v1/54693b1ee4b07c8a3da7b6d0/1492201646122-7CCYPRAF33QU2MGPW8YJ/Headshot-by-Lamonte-G-Photography-Baltimore-Corporate-Headshot-Photographer-IMG_6763-Edit.JPG?format=1500w",
    },
  ];
  const [selectFriend, setSelectFriend] = useState(1);
  const [peers, setPeers] = useState(staticPeers);
  const [bill, setBill] = useState({
    totalBill: 0,
    myExpense: 0,
    iPay: "you",
  });

  let peerBill = bill.totalBill - bill.myExpense;

  const onSelectFriend = (index) => {
    setSelectFriend(index);
    if (selectFriend === index) setSelectFriend(null);
  };

  const onAddPeer = (peerName, peerPic) => {
    if (peerName && peerPic)
      setPeers([
        ...peers,
        {
          name: peerName,
          money: 0,
          selected: false,
          profilePic: peerPic,
        },
      ]);
  };
  // using Object.assign to modify the money state of the selected Friend
  /*

! Calcs are decided based upon the who pays state, this state will be toggled in the select below
*/
  const handleMoney = () => {
    if (bill.iPay === "you")
      setPeers(
        peers.map((i, index) =>
          index === selectFriend
            ? Object.assign(i, { ...i, money: i.money + peerBill })
            : { ...i }
        )
      );
  };
  if (bill.iPay === "peer")
    setPeers(
      peers.map((i, index) =>
        index === selectFriend
          ? Object.assign(i, { ...i, money: i.money - peerBill })
          : { ...i }
      )
    );

  // ! infinite loop caused by setting state with the onChange event
  // waiting for answer from forum to proceed here

  // const handleWhoPays = (e, id) => {
  //   const selectTag = document.getElementById(id);

  //   if(selectTag.value === )
  //   setBill({...bill, iPay: e.target.value})

  // };


  return (
    <div className="App">
      <div className="peer-container">
        {peers.map((i, index) => {
          return (
            <PeerCard
              // key={index}
              handleSelectFriend={onSelectFriend}
              peers={peers}
              index={index}
              name={i.name}
              money={i.money}
              selected={selectFriend}
              profilePic={i.profilePic}
            />
          );
        })}

        <AddPeer handleAddPeer={onAddPeer} />
        <button className="mt4" id="close-btn">
          Close
        </button>
      </div>
      <div className="bill-total-container">
        {peers.map((i, index) => {
          // this will likely change
          if (index === selectFriend)
            return (
              <>
                <h1 className="mt3 mb3">Split a bill with {i.name}</h1>
                <BillRow>
                  <span>Bill value</span>
                  <input
                    onChange={(e) =>
                      setBill({
                        ...bill,
                        totalBill: Number(e.target.value),
                      })
                    }
                    type="number"
                    id="bill-value"
                  />
                </BillRow>
                <BillRow>
                  <span>Your Expense</span>
                  <input
                    onChange={(e) =>
                      setBill({
                        ...bill,
                        myExpense: Number(e.target.value),
                      })
                    }
                    type="number"
                    id="your-expense"
                  />
                </BillRow>
                <BillRow>
                  <span>{i.name}'s Expense</span>
                  <input value={peerBill} type="text" id="thier-expense" />
                </BillRow>




 {/* !!! THIS IS THE PROBLEM AREA !!! */}
                <BillRow>
                  <span>Who is paying the bill?</span>
                  <select
                    name={bill.iPay}
                    value={bill.iPay}
                    onChange={(e) =>
                      // console.log(e.target.value)
                      setBill({ ...bill, iPay: e.target.value })
                    }
                    style={{ cursor: "pointer" }}
                    id="who-pays"
                  >
                    {/* <option ></option> */}
                    <option value="you">You</option>
                    <option value="peer">{i.name}</option>
                  </select>
                </BillRow>
{/* !!! THIS IS THE PROBLEM AREA !!! */}


                <div className="split-bill-container">
                  <button onClick={handleMoney}>Split Bill</button>
                </div>
              </>
            );
        })}
        {selectFriend === null && (
          <h1 style={{ textAlign: "center" }}>
            Select a Friend to Split the Bill With!
          </h1>
        )}
      </div>
    </div>
  );
}

export default App;

Here is the BillRow component, all it does is render the children

export const BillRow = ({children}) => {
  return (
    <div className='bill-row-container mt3 mb3'>
      {children}
    </div>
  )
}

I would like to know how I can set the state to the currently selected value of the select element without an infinite loop or unnecessary re-renders.

I have also tried e.preventDefault in the onChange, but it was to no avail

hosted app
my github repo

UPDATE: I noticed that when I made the “iPay” state its own separate state instead of including it in the bill state object that the infinite loop issue went away. I have no idea why that is. I am still new to react and very new to the concept of putting state into complex objects instead of making a lot of individual state. Can anyone explain to me why that is?

  • in react you dont have to use this kind of dom targeting
    for “onchange” hanlder
  • rather simply pass on an “event” and get value from it directly
  • what “code change” you brought in “before” and “after” fix

happy coding :slight_smile:

1 Like

Thank you for the clarification on event handlers in React, it makes much more sense now. Could you explain in more detail about the “code change” “before” and “after”? I’m sorry but I am not following what that means exactly.

  • you said “inifinte loop” went away, where previously there was an “infinite loop” present in code, show that “before” and “after” changes you have made to remove that

please let me know, if there is any more question about this, thanks and happy coding :slight_smile:

1 Like

Oh okay thank you for clarifying !

When the infinite loop was occurring I was updating state from this state object

const [bill, setBill] = useState({
totalBill: '',
myExpense: '',
iPay: 'you'
})

When I made iPay its own separate state, then the infinite loop error went away

const [iPay, setIPay] = useState('you');

thanks, but i get that what you mean, what i was asking is that when you are “updating” how you were doing that!!

in general if you update “state” directly from component and not from any "event handler or “useEffect” you are likely to have an “infinite” loop

looking at this SO thread might be helpful,also try looking at “react official docs” they are pretty awesome as well happy coding :slight_smile: javascript - Updating and merging state object using React useState() hook - Stack Overflow

1 Like

Ohhh okay I see, thank you for being patient! I was updating state in an onChange event handler for the select element. The only place I used a useEffect was for setting a disabled class for the button to calculate payment.

When I would make the select element an uncontrolled element the error would also go away, but of course then I could not update the value.

This is what the element looked like before when the infinite loop was being triggered

<select
                    name={bill.iPay}
                    value={bill.iPay}
                    onChange={(e) =>
                      setBill({ ...bill, iPay: e.target.value })
                    }
                    style={{ cursor: "pointer" }}
                    id="who-pays"
                  >
                    {/* <option ></option> */}
                    <option value="you">You</option>
                    <option value="peer">{i.name}</option>
                  </select>

and this is what it looked like after when I no longer got an infinite loop error. I did not change anything else

 <select
                    name={whoPays}
                    value={whoPays}
                    onChange={(e) => {
                      setWhoPays(e.target.value);
                    } }
                    style={{ cursor: "pointer" }}
                    id="who-pays"
                  >

thank you for sharing the reference and the advice of where else to look. I will be sure to study up on it.

what if you have tried something this setBill(prevState => ({...prevState, iPay: e.target.value}) ) would that stop having an infinite loop

fyi, i dont see any real problem in it “to cause” any infinite loop in your attempted code

glad to help, happy coding :slight_smile:

1 Like

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.