0

This is a simple form table with two states - one (employees) is to keep track of the updated form value, and the other (records) should have saved the form values only when the add/save button is clicked.

The issue is that whenever handleChange is called, it starts to update both the states, which is very weird since setRecords is not called in there.

what am I doing wrong that causes the two states to change together?

I suspect that it has something to do with how I initialized the two states here. What is the correct way to do it then?

  const [employees, setEmployees] = useState([...initialValues, emptyRow] || []);
  const [records, setRecords] = useState([...initialValues] || []);

Full codes are here:

import { useState } from "react";
import "./App.css";

const initialValues = [
  {
    id: 1,
    name: "John",
    position: "Developer",
    salary: 50000,
    isUpdated: false,
  },
  {
    id: 2,
    name: "Jane",
    position: "Designer",
    salary: 60000,
    isUpdated: false,
  },
  {
    id: 3,
    name: "Bob",
    position: "Manager",
    salary: 70000,
    isUpdated: false,
  },
];

const emptyRow = {
  id: 0,
  name: "",
  position: "",
  salary: 0,
  isUpdated: false,
};

const headings = ["Name", "Position", "Salary", "Action"];

const Employee = ({ employee, handleChange, handleClick }) => (
  <tr key={employee.id}>
    <td>
      <input
        type="text"
        id="name"
        name="name"
        onChange={(e) => handleChange(e, employee.id)}
        value={employee.name}
        disabled={employee.id !== 0}
      />
    </td>
    <td>
      <input
        type="text"
        id="position"
        name="position"
        onChange={(e) => handleChange(e, employee.id)}
        value={employee.position}
        disabled={employee.id !== 0}
      />
    </td>
    <td>
      <input
        type="number"
        id="salary"
        name="salary"
        onChange={(e) => handleChange(e, employee.id)}
        value={employee.salary ? employee.salary : ""}
      />
    </td>
    <td>
      <button onClick={() => handleClick(employee.id)}>{employee.id !== 0 ? "Save" : "Add"}</button>
    </td>
  </tr>
);

function App() {
  const [employees, setEmployees] = useState([...initialValues, emptyRow] || []);
  const [records, setRecords] = useState([...initialValues] || []);

  const handleClick = (id) => {
    const foundEmp = employees.find((x) => x.id === id);
    const foundRec = records.find((x) => x.id === id);
    console.log(`------- foundEmp :`, JSON.stringify(foundEmp, null, 2));
    console.log(`------- foundRec :`, JSON.stringify(foundRec, null, 2));
  };

  const handleChange = (e, id) => {
    const k = e.target.name;
    const v = e.target.value;
    const updated = employees.map((emp) => {
      if (emp.id === id) {
        emp[k] = v;
      }
      return emp;
    });
    setEmployees([...updated]);
  };

  return (
    <div className="App">
      <form onSubmit={(e) => e.preventDefault()}>
        <table width="100%" data-testid="table">
          <tbody>
            <tr>
              {headings?.map((h, i) => (
                <th key={i}>{h}</th>
              ))}
            </tr>
            {employees.map((employee) => (
              <Employee employee={employee} handleChange={handleChange} handleClick={handleClick} />
            ))}
          </tbody>
        </table>
      </form>
    </div>
  );
}

export default App;

enter image description here

Zoe L
  • 1,150
  • 14
  • 22
  • 2
    `emp[k] = v;` changes the `emp` object directly in your array - see [Why they didn't change property directly in map() in Redux tutorial?](https://stackoverflow.com/a/66654421/5648954) – Nick Parsons Mar 30 '23 at 02:49

1 Answers1

3

Right here you are mutating the original object:

const handleChange = (e, id) => {
    const k = e.target.name;
    const v = e.target.value;
    const updated = employees.map((emp) => {
      if (emp.id === id) {
        emp[k] = v; // HERE!!
      }
      return emp;
    });
    setEmployees([...updated]);
  };

Note objects in JS are always passed by reference. Both arrays are holding references to the same objects.

You should instead create a new employee so as not to mutate the original:

const handleChange = (e, id) => {
    const k = e.target.name;
    const v = e.target.value;
    const updated = employees.map((emp) => {
      if (emp.id === id) {
        newEmp = {...emp};
        newEmp[k] = v;
        return newEmp;
      }
      return emp;
    });
    setEmployees(updated); // Spread operator is not necessary
  };

Note that the spread operator {...emp} only duplicates the object one level deep. If one of the object's properties was another object, the spread operator would not duplicate the nested object. Sometimes it's better to duplicate your objects before passing them as the default value to useState. Generally you'd use a class constructor or a function for that. Something to keep in mind.

Chris Hamilton
  • 9,252
  • 1
  • 9
  • 26
  • It works! Thank you! Following up: const updated = [...employees].map((emp) => { if (emp.id === id) { emp[k] = v; } return emp; }); setEmployees([...updated]); Wondering why duplicating array this won't work the same? – Zoe L Mar 30 '23 at 03:01
  • 1
    Neither of the arrays hold the objects, they hold *pointers* to the objects. They both point to the same objects. So mutating an object in one array affects the other. The array spread operator `[...updated]` creates a new *array* with the same pointer values, not new objects. – Chris Hamilton Mar 30 '23 at 03:03
  • @ZoeL note the `[...updated]` step is not necessary, since `map` creates a new array anyway. I removed it from the answer. – Chris Hamilton Mar 30 '23 at 03:13
  • 1
    Good answer. Note that [there is no such thing as spread operator](https://stackoverflow.com/questions/44934828/is-it-spread-syntax-or-the-spread-operator), though. – Unmitigated Mar 30 '23 at 03:24
  • @Unmitigated neat, thanks for the tip. – Chris Hamilton Mar 30 '23 at 03:28