Search code examples
javascriptreactjs

React UI not re-rendering with state update


I'm trying to create an expenses track app using react JS. The problem is that I'm updating the state of the variable, but the UI doesn't change accordingly. For each new expense added, the idea is that:

  1. If the expense name already exists, a new line is not created, instead, the value of the new expense is added to the existing value;
  2. If the expense name does not exist, a new expense line is created. Logging to the console, I can see that for the case a) the variable groupedExpenses is updated, but the UI of the expense line isn't updated.

Below is my code:

  1. App.jsx
import Expenses from "./components/Expenses";
import Form from "./components/Form";
import { useState } from "react";

function App() {
  const [groupedExpenses, setGroupedExpenses] = useState({});

  function addExpense(name, amount) {
    setGroupedExpenses((previousGroupedExpenses) => {
      let newGroupedExpenses = { ...previousGroupedExpenses };
      console.log(newGroupedExpenses);
      if (name in newGroupedExpenses) {
        newGroupedExpenses[name] += parseFloat(amount.replace(",", "."));
      } else {
        newGroupedExpenses[name] = parseFloat(amount.replace(",", "."));
      }
      return newGroupedExpenses;
    });
  }

  return (
    <div className="expensesapp stack-large">
      <h1>Expense Manager</h1>
      <Form addExpense={addExpense} />
      <Expenses groupedExpenses={groupedExpenses} />
    </div>
  );
}

export default App;

  1. Form.jsx
import { useState } from "react";

import TextField from "@mui/material/TextField";
import Stack from "@mui/material/Stack";
import Autocomplete from "@mui/material/Autocomplete";
import InputAdornment from "@mui/material/InputAdornment";
import Icon from "@mui/material/Icon";

function Form(props) {
  const [expenseName, setExpenseName] = useState("");
  const [expenseAmount, setExpenseAmount] = useState("");

  const [options, setOptions] = useState([]);

  function handleChangeName(event) {
    setExpenseName(event.target.value);
  }

  function handleSelectSuggestion(event, value) {
    setExpenseName(value);
  }

  function handleChangeAmount(event) {
    setExpenseAmount(event.target.value);
  }

  function handleSubmit(event) {
    event.preventDefault();
    props.addExpense(expenseName, expenseAmount);

    if (!options.includes(expenseName)) {
      setOptions([...options, expenseName]);
    }

    setExpenseName("");
    setExpenseAmount("");
  }

  return (
    <form onSubmit={handleSubmit}>
      <div className="expense">
        <Stack direction="row" spacing={1} sx={{ width: 300 }}>
          <Autocomplete
            sx={{ m: 1, width: "25ch" }}
            id="free-solo"
            freeSolo
            onInputChange={handleChangeName}
            onChange={handleSelectSuggestion}
            options={options}
            renderInput={(params) => (
              <TextField {...params} label="nome" value={expenseName} />
            )}
          />
          <TextField
            label="valor"
            id="outlined-start-adornment"
            sx={{ m: 1, width: "25ch" }}
            onChange={handleChangeAmount}
            value={expenseAmount}
            slotProps={{
              input: {
                startAdornment: (
                  <InputAdornment position="start">R$</InputAdornment>
                ),
              },
            }}
          />
          <button type="submit" className="btn btn__primary btn__lg">
            <Icon>add_circle</Icon>
          </button>
        </Stack>
      </div>
    </form>
  );
}

export default Form;
  1. Expenses.jsx
function Expenses(props) {
  console.log(props.groupedExpenses);

  return (
    <ul
      role="list"
      className="expenses-list stack-large stack-exception"
      aria-labelledby="list-heading"
    >
      {props.groupedExpenses !== undefined &&
        Object.entries(props.groupedExpenses).map(([k, v]) => (
          <div>
            <li className="expense stack-small">
              <input
                type="text"
                className="input input-name input__lg"
                name="name"
                autoComplete="off"
                defaultValue={k}
                readOnly
              />
              <input
                type="text"
                className="input input-value input__lg"
                name="value"
                autoComplete="off"
                defaultValue={v}
                readOnly
              />
            </li>
          </div>
        ))}
    </ul>
  );
}

export default Expenses;

Could anyone point out what am I doing wrong? Thanks!


Solution

  • The issue that you're facing is because of defaultValue in Expenses.jsx

    defaultValue is used to set an initial value. If the state changes, the defaultValue won't trigger the re-render. That's the reason it's not reflecting in the UI. Since it's not re-rendered, it's not updating in the UI

    Solution

    You can use value instead of defaultValue. This should resolve the issue.