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;
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.