alin11g
alin11g

Reputation: 35

Array with inputs in React

Im trying to create a table with an array of products, the problem I have is that the inputs take the value of any input with the same "name". Also, when I try to remove any of the products, it always removes the last one.

https://stackblitz.com/edit/react-gto9bw?file=src/App.js

const [producto, setProducto] = useState({
codigo: '',
nombre: '',
descripcion: '',
precio: '',
cantidad: '',
estado: '',


});

  const [productos, setProductos] = useState([]);

  const addProducto = () => {
    setProductos([...productos, producto]);
  };

  const removeProducto = (e, index) => {
    e.preventDefault();
    const list = [...productos];
    list.splice(index, 1);
    setProductos(list);
  };

  const handleInputChangeProducto = (e, index) => {
    e.preventDefault();
    const { name, value } = e.target;
    const list = [...productos];
    list[index][name] = value;
    setProductos(list);
  };

The return its a table that has a button to add the product

return (
<div>
  <table className="table-size" style={{ border: '1px solid black' }}>
    <thead>
      <tr>
        <th scope="col">Nombre</th>
        <th scope="col">Descripcion</th>
      </tr>
      {productos.map((producto, index) => (
        <tr key={index}>
          <td>
            <input
              name="nombre"
              onChange={(e) => handleInputChangeProducto(e, index)}
            />
          </td>
          <td>
            <input
              name="descripcion"
              onChange={(e) => handleInputChangeProducto(e, index)}
            />
          </td>
          <td onClick={(e) => removeProducto(e, index)}>
            <Button>Borrar Producto {index}</Button>
            
          </td>
        </tr>
      ))}
    </thead>
  </table>
  <br />

  <button onClick={addProducto}>Crear Producto</button>
</div>

I've tried separating the "tr" into a separate component but that doesn't work either.

Upvotes: 1

Views: 160

Answers (5)

Andrew Stegmaier
Andrew Stegmaier

Reputation: 3787

Although the other answers are not wrong - and they all (implicitly) fixed the issues below, I wanted to call out the root cause of the problem you were seeing. It's not the use of indexes as keys (although having unique keys is a good idea for performance reasons).

  1. When you created a new producto (at least in your original code sample), you were doing this:

    const addProducto = () => {
        setProductos([...productos, producto]);
    };
    

    Note the new producto being added to the array is always the same object. That means that your array contains a list of pointers that are all pointing to the same object. Modifying one will modify all of them. That's probably not what you want. Instead, do this:

    const addProducto = () => {
        setProductos([...productos, {...producto} ]);
    };
    

    That spreads the properties of your blank producto object onto a new object and ensures that each object in the array is really a separate thing.

  2. The form's values were not actually controlled by the state. Going from your original code sample, make these changes:

       {productos.map((producto, index) => (
         <tr key={index}>
           <td>
             <input
               name="nombre"
               value={producto.nombre} // <-- this was missing. without it there is no relation between what the user is seeing in the input and what value is stored in the productos array.
               onChange={(e) => handleInputChangeProducto(e, index)}
             />
           </td>
           <td>
             <input
               name="descripcion"
               value={producto.descripcion} // <-- Same thing here
               onChange={(e) => handleInputChangeProducto(e, index)}
             />
           </td>
    

Upvotes: 2

dismedia
dismedia

Reputation: 129

const addProducto = () => {
    setProductos((list) => [
      ...list,
      {
        id: list.length, //this should be unique anyway
        codigo: '',
        nombre: '',
        descripcion: '',
        precio: '',
        cantidad: '',
        estado: '',
      },
    ]);
  };

  const removeProducto = (e, id) => {
    setProductos((list) => {
      list.splice(id, 1);
      return [...list]
    });
  };

you should change your list in callback manner as shown.

Upvotes: 0

Liki Crus
Liki Crus

Reputation: 2062

This is a common issue when using map with index instead of unique key.

So you can try like this:

Please add a global variable key in your case.

let key = 0;

Then set this key in your producto and increase it. (In my full code, I used codigo as a key field.)

It should always be increased.

On backend API, you should get unique key as well.

Here is a full code.

import React, { useState } from 'react';
import { Button } from 'react-bootstrap';
import './style.css';

let key = 0;

export default function App() {
  const [producto, setProducto] = useState({
    codigo: '',
    nombre: '',
    descripcion: '',
    precio: '',
    cantidad: '',
    estado: '',
  });

  const [productos, setProductos] = useState([]);

  const addProducto = () => {
    setProductos([...productos, { ...producto, codigo: ++key }]);
  };

  const removeProducto = (e, index) => {
    e.preventDefault();
    const list = [...productos];
    list.splice(index, 1);
    setProductos(list);
  };

  const handleInputChangeProducto = (e, index) => {
    e.preventDefault();
    const { name, value } = e.target;
    const list = [...productos];
    list[index][name] = value;
    setProductos(list);
  };

  console.log({ productos });

  return (
    <div>
      <table className="table-size" style={{ border: '1px solid black' }}>
        <thead>
          <tr>
            <th scope="col">Nombre</th>
            <th scope="col">Descripcion</th>
          </tr>
          {productos.map((producto, index) => (
            <tr key={producto.codigo}>
              <td>
                <input
                  name="nombre"
                  onChange={(e) => handleInputChangeProducto(e, index)}
                />
              </td>
              <td>
                <input
                  name="descripcion"
                  onChange={(e) => handleInputChangeProducto(e, index)}
                />
              </td>
              <td onClick={(e) => removeProducto(e, index)}>
                <Button>Borrar Producto {index}</Button>
              </td>
            </tr>
          ))}
        </thead>
      </table>
      <br />

      <button onClick={addProducto}>Crear Producto</button>
    </div>
  );
}

Upvotes: 2

Pavan Aditya M S
Pavan Aditya M S

Reputation: 375

Add an id field to your product JSON.

const [producto, setProducto] = useState({
   id: '',
   codigo: '',
   nombre: '',
   descripcion: '',
   precio: '',
   cantidad: '',
   estado: '',
});

Update the ID for every product addition,

const addProducto = () => {
   setProductos([...productos, {id: (prodcutos.length + 1), ...producto}]);
};

Replace your current key from index to producto.id,

{productos.map((producto, index) => (
    <tr key={producto.id}>

Pass this id as param for you handleInputChangeProducto function,

      <td>
        <input
          name="nombre"
          onChange={(e) => handleInputChangeProducto(e, producto.id)}
        />
      </td>
      <td>
        <input
          name="descripcion"
          onChange={(e) => handleInputChangeProducto(e, producto.id)}
        />
      </td>
      <td onClick={(e) => removeProducto(e, producto.id)}>
        <Button>Borrar Producto {index}</Button>
        
      </td>

Upvotes: 1

Gabriel Alc&#226;ntara
Gabriel Alc&#226;ntara

Reputation: 626

You need an unique key. Try to generate this unique key on your addProducto:

const addProducto = () => {
    setProductos([...productos, { ...producto, id: Date.now() }]);
};

And then on your productos.map pass this generated id in your <tr key={producto.id}>.

Its recommended you have "static" indexes as key, React will understand better how manipulate your list and if occurs any modifications in this list, he will not recalculate everything.

Another point, if you need to manipulate something that depends of your previous state, like these add or remove operations, try to use functions for updates, like:

const addProducto = () => {
    setProductos(prevProductos => [...prevProductos, { ...producto, id: Date.now() }]);
};

const removeProducto = (e, index) => {
    e.preventDefault();
    setProductos(prevProductos => [...prevProductos.slice(0, index), ...prevProductos.slice(index + 1)]);
};

Upvotes: 1

Related Questions