1

When I add an expense or income in my React budget app, there is a delay in calculating the totals. In adding a new item (or clicking add new item with the input fields empty) the total is shown from the previous calculation.

I'm sure it's related to the fact that setState doesn't mutate state immediately and that it might help to use the previous state, but I can't see how I'm able to use previous state to perform a new count of the totals.

Thanks in advance for any help!

Code below and app can be found here - https://codesandbox.io/s/budget-app-react-n2vfp?file=/src/App.js:0-3786

APP.js

import React, { Component } from "react";
import "./styles.css";
import Incomes from "./components/Incomes";
import Expenses from "./components/Expenses";
import TotalInc from "./components/TotalInc";

class App extends Component {
  state = {
    newAmount: "",
    newDescription: "",
    newType: "select",

    inc: [],
    exp: [],

    totalInc: 0,
    totalExp: 0
  };

  handleChange = (evt) => {
    const value = evt.target.value;

    this.setState({ [evt.target.name]: value });
    console.log(
      this.state.newAmount,
      this.state.newDescription,
      this.state.newType
    );

      };
    
      calculateTotal = (type) => {
        let sum = 0;
        this.state[type].forEach((cur) => {
          sum += cur.amount;
        });
    
        if (type === "inc") {
          this.setState( { totalInc: sum });
        } else if (type === "exp") {
          this.setState( { totalExp: sum });
        }
      };
    
      addItem = () => {
        if (this.state.newAmount && this.state.newDescription)
          if (this.state.newType === "inc") {
            this.setState({
              inc: [
                ...this.state.inc,
                {
                  amount: parseFloat(this.state.newAmount),
                  description: this.state.newDescription
                }
              ]
            });
          } else if (this.state.newType === "exp") {
            this.setState({
              exp: [
                ...this.state.exp,
                {
                  amount: parseFloat(this.state.newAmount),
                  description: this.state.newDescription
                }
              ]
            });
          }
    
        this.setState({
          newAmount: "",
          newDescription: "",
          newType: "select"
        });
    
        this.calculateTotal("inc");
        this.calculateTotal("exp");
      };
    
      deleteItem = () => {};
    
      render() {
        return (
          <div>
            <div className="MainContainer">
              <h1 className="Header">Budget App - React</h1>
              <div className="InputSection">
                <p> Add an item! </p>
                <div className="InputFields">
                  <input
                    name="newAmount"
                    placeholder="Amount"
                    value={this.state.newAmount}
                    onChange={this.handleChange}
                  />
    
                  <input
                    name="newDescription"
                    placeholder="Description"
                    value={this.state.newDescription}
                    onChange={this.handleChange}
                  />
    
                  <select
                    name="newType"
                    value={this.state.newType}
                    onChange={this.handleChange}
                  >
                    <option value="select" selected>
                      {" "}
                    </option>
                    <option value="inc">+</option>
                    <option value="exp">-</option>
                  </select>
                  <br />
                </div>
                <div className="addButton">
                  <button onClick={this.addItem}> Add an item </button>
                </div>
              </div>
    
              <div className="itemsContainer">
                <div className="incomeContainer">
                  <Incomes incomes={this.state.inc} />
                </div>
                <div className="expensesContainer">
                  <Expenses expenses={this.state.exp} />
                </div>
              </div>
    
              <div className="totalsContainer">
                <div className="incTotals">
                  Total Incomes: {this.state.totalInc}
                  
                </div>
                <div className="expTotals">
                  Total Expenses: {this.state.totalExp}
                </div>
              </div>
            </div>
          </div>
        );
      }
    }
    
    export default App;

INCOMES.js

import React, { Component } from "react";

const Income = (props) => {
  return (
    <div className="incomeListItem">
      {props.amount}
      <span> - </span>
      {props.description}
      <button> X </button>
      <hr />
    </div>
  );
};

class Incomes extends React.Component {
  render() {
    return (
      <div>
        <h2 className="IncomeHeader"> Incomes</h2>
        <div>
          {this.props.incomes.map(({ amount, description }) => {
            return (
              <Income
                key={Math.random()}
                amount={amount}
                description={description}
              />
            );
          })}
        </div>
      </div>
    );
  }
}

export default Incomes;

EXPENSES.js

import React, { Component } from "react";

const Expense = props => {
  return (
    <div className="expenseListItem">
      {props.amount}
      <span> - </span>
      {props.description}
      <p> Percentage to go here </p>
      <button> X </button>
    </div>
  );
};

class Expenses extends React.Component {
  render() {
    return (
      <div>
        <h2> Expenses </h2>
        <div>
          {this.props.expenses.map(({ amount, description }) => {
            return (
              <Expense
                key={Math.random()}
                amount={amount}
                description={description}
              />
            );
          })}
        </div>
      </div>
    );
  }
}

export default Expenses;
Janez Kuhar
  • 3,705
  • 4
  • 22
  • 45
Jimthor
  • 23
  • 5

1 Answers1

1

You have to somehow "pack" all the state updates, that you want to happen at the same time, into a single call to this.setState().

Edit your addItem() implementation in App.js like so:

addItem = () => {
  let updatedIncomes = this.state.inc;
  let updatedExpenses = this.state.exp;

  if (this.state.newAmount && this.state.newDescription) {
    if (this.state.newType === "inc") {
      updatedIncomes = [
        ...this.state.inc,
        {
          amount: parseFloat(this.state.newAmount),
          description: this.state.newDescription
        }
      ];
    } else if (this.state.newType === "exp") {
      updatedExpenses = [
        ...this.state.exp,
        {
          amount: parseFloat(this.state.newAmount),
          description: this.state.newDescription
        }
      ];
    }
  }

  this.setState({
    newAmount: "",
    newDescription: "",
    newType: "select",
    inc: updatedIncomes,
    exp: updatedExpenses,
    totalInc: updatedIncomes.reduce((acum, e) => acum + e.amount, 0),
    totalExp: updatedExpenses.reduce((acum, e) => acum + e.amount, 0)
  });
};

Note that this is just a quick fix. A more robust solution would be to try and make your components a bit more useful rather than just feeding them the data to render.

For example, you could make a new component for the pink section of your UI that would receive inc and dec arrays as props and then that component would know how to compute the totals. This would simplify your App state.

Janez Kuhar
  • 3,705
  • 4
  • 22
  • 45
  • Thanks again for your help on this. Just to follow up, how could you be sure that this new setState call would update the state immediately, as intended, whereas my smaller ones didn't? Is it a question of "critical mass", like packing a minimum or certain number of updates into a setState call to guarantee that it will happen immediately? I'm finding the unpredictability of React state updates a bit tricky and would be grateful for any guidelines as to what makes one setState call more likely to happen immediately than another. I hope my question makes sense! – Jimthor Jun 18 '21 at 02:19
  • @Jimthro See this question for more info on that: [Why calling react setState method doesn't mutate the state immediately?](https://stackoverflow.com/questions/30782948/why-calling-react-setstate-method-doesnt-mutate-the-state-immediately) – Janez Kuhar Jun 18 '21 at 05:28