skip to Main Content

I’m trying to create a simple macronutrient calculator in React which has multiple input fields that require the user to input numeric values in order to do some calculations. The current problem is that if the user fills the "grams" input field last, calculations will not work unless he changes each one of the other fields. gramsEaten variable is used in each calculation and it always needs to be filled first to work properly.

const [gramsEaten, setGramsEaten] = useState(0);
const [totalKcal, setTotalKcal] = useState(0);
const [totalCarbs, setTotalCarbs] = useState(0);
const [totalFats, setTotalFats] = useState(0);
const [totalProtein, setTotalProtein] = useState(0);

let handleGramsEatenChange = (e) => {
    let grams = e.target.value;
    setGramsEaten(grams);
}

let handleKcalChange = (e) => {
    let kcal = e.target.value;
    setTotalKcal((gramsEaten * kcal) / 100);
}

let handleCarbsChange = (e) => {
    let carbs = e.target.value;
    setTotalCarbs((gramsEaten * carbs) / 100);
}

let handleFatsChange = (e) => {
    let fats = e.target.value;
    setTotalFats((gramsEaten * fats) / 100);
}

let handleProteinChange = (e) => {
    let protein = e.target.value;
    setTotalProtein((gramsEaten * protein) / 100);
}

And the order of the input fields & results as it goes:

<input type="number" min="0" onChange={handleKcalChange}/>
<input type="number" min="0" onChange={handleCarbsChange}/>
<input type="number" min="0" onChange={handleFatsChange}/>
<input type="number" min="0" onChange={handleProteinChange}/>

<label>How many grams have you consumed?</label>
<input type="number" min="0" onChange={handleGramsEatenChange}/>

<table>
    <tbody>
    <tr>
        <th>Total calories</th>
        <td>{Math.ceil(totalKcal)}</td>
    </tr>
    <tr>
        <th>Total carbohydrates</th>
        <td>{Math.ceil(totalCarbs)}</td>
    </tr>
    <tr>
        <th>Total fats</th>
        <td>{Math.ceil(totalFats)}</td>
    </tr>
    <tr>
        <th>Total protein</th>
        <td>{Math.ceil(totalProtein)}</td>
    </tr>
    </tbody>
</table>

2

Answers


  1. The solution to this might be to store only the state of inputs. Do not compute the totals first.

    const [gramsEaten, setGramsEaten] = useState(0);
    const [kcal, setKcal] = useState(0);
    const [carbs, setCarbs] = useState(0);
    const [fats, setFats] = useState(0);
    const [protein, setProtein] = useState(0);
    
    let handleGramsEatenChange = (e) => {
        setGramsEaten(e.target.value);
    }
    
    let handleKcalChange = (e) => {
        setKcal(e.target.value);
    }
    
    let handleCarbsChange = (e) => {
        setCarbs(e.target.value);
    }
    
    let handleFatsChange = (e) => {
        setFats(e.target.value);
    }
    
    let handleProteinChange = (e) => {
        setProtein(e.target.value)
    }
    

    Then, at the view, you can do your computation there.Values might show NaN when user inputs a string or some invalid non-number, so its up to you to do the validation of inputs. In the view, you could also do conditionals to hide the value if the constituent value needed for the computation is not entered yet, as shown with the "kcal == undefined && gramsEaten !== undefined && Math.ceil((kcal*gramsEaten)/100)".

    <input type="number" min="0" onChange={handleKcalChange}/>
    <input type="number" min="0" onChange={handleCarbsChange}/>
    <input type="number" min="0" onChange={handleFatsChange}/>
    <input type="number" min="0" onChange={handleProteinChange}/>
    
    <label>How many grams have you consumed?</label>
    <input type="number" min="0" onChange={handleGramsEatenChange}/>
    
    <table>
        <tbody>
        <tr>
            <th>Total calories</th>
            <td>{kcal == undefined && gramsEaten !== undefined && Math.ceil((kcal*gramsEaten)/100)}</td>
        </tr>
        <tr>
            <th>Total carbohydrates</th>
            <td>{Math.ceil(carbs*gramsEaten)/100}</td>
        </tr>
        <tr>
            <th>Total fats</th>
            <td>{Math.ceil(fats*gramsEaten/100)}</td>
        </tr>
        <tr>
            <th>Total protein</th>
            <td>{Math.ceil(protein*gramsEaten)/100}</td>
        </tr>
        </tbody>
    </table>
    

    A recommendation for you would also be to store all related state in an object, instead of individual useStates.

    const [macronutrientState, setMacronutrientState] = useState({fats: undefined, carbs: undefined, gramsEaten: undefined, kcal: undefined})
    
    const handleMacronutrientStateChange = (e) => {
        setMacronutrientState((prevMacronutrientState) => 
            ({...prevMacronutrientState, 
                 [e.target.name]: e.target.value
             }
            )
        )
    }
    
    <input type="number" min="0" name="kcal" onChange={handleMacronutrientChange}/>
    <input type="number" min="0" name="carbs" onChange={handleMacronutrientChange}/>
    <input type="number" min="0" name="fats" onChange={handleMacronutrientChange}/>
    <input type="number" min="0" name="protein" onChange={handleMacronutrientChange}/>
    
    <label>How many grams have you consumed?</label>
    <input type="number" min="0" name="gramsEaten" onChange={handleMacronutrientChange}/>
    
    <table>
        <tbody>
        <tr>
            <th>Total calories</th>
            <td>{Math.ceil((macronutrientState.kcal*macronutrientState.gramsEaten)/100))}</td>
        </tr>
        <tr>
            <th>Total carbohydrates</th>
            <td>{Math.ceil((macronutrientState.carbs*macronutrientState.gramsEaten)/100))}</td>
        </tr>
        <tr>
            <th>Total fats</th>
            <td>{Math.ceil((macronutrientState.fats*macronutrientState.gramsEaten)/100))}</td>
        </tr>
        <tr>
            <th>Total protein</th>
            <td>{Math.ceil((macronutrientState.protein*macronutrientState.gramsEaten)/100))}</td>
        </tr>
        </tbody>
    </table>
    

    This is good practice as it helps you organise ur state, and reuse the same function handleMacronutrientStateChange, which sets state based on the name given in the input.

    Login or Signup to reply.
  2. When you call your setGramsEaten in handleGramsEatenChange, you only updated the gramsEaten state but did not update the other states.

    Solution: Move your calculations outside of state and only use state for the variables needed for the calculations.

      const [gramsEaten, setGramsEaten] = useState(0);
      const [kcal, setKcal] = useState(0);
      const [carbs, setCarbs] = useState(0);
      const [fats, setFats] = useState(0);
      const [protein, setProtein] = useState(0);
    
      let handleGramsEatenChange = (e) => {
        let grams = e.target.value;
        setGramsEaten(grams);
      };
    
      let handleKcalChange = (e) => {
        let kcal = e.target.value;
        setKcal(kcal);
      };
    
      let handleCarbsChange = (e) => {
        let carbs = e.target.value;
        setCarbs(carbs);
      };
    
      let handleFatsChange = (e) => {
        let fats = e.target.value;
        setFats(fats);
      };
    
      let handleProteinChange = (e) => {
        let protein = e.target.value;
        setProtein(protein);
      };
    
      // the second half of the code you provided
    
      <input type="number" min="0" onChange={handleKcalChange} />
      <input type="number" min="0" onChange={handleCarbsChange} />
      <input type="number" min="0" onChange={handleFatsChange} />
      <input type="number" min="0" onChange={handleProteinChange} />
    
      <label>How many grams have you consumed?</label>
      <input type="number" min="0" onChange={handleGramsEatenChange} />
    
      
      <table>
        <tbody>
          <tr>
            <th>Total calories</th>
            <td>{Math.ceil((gramsEaten * kcal) / 100)}</td>
          </tr>
          <tr>
            <th>Total carbohydrates</th>
            <td>{Math.ceil((gramsEaten * carbs) / 100)}</td>
          </tr>
          <tr>
            <th>Total fats</th>
            <td>{Math.ceil((gramsEaten * fats) / 100)}</td>
          </tr>
          <tr>
            <th>Total protein</th>
            <td>{Math.ceil(gramsEaten * protein) / 100}</td>
          </tr>
        </tbody>
      </table>
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search