Artículo original escrito por Yogesh Chavan
Artículo original React Tutorial – How to Work with Multiple Checkboxes
Traducido y adaptado por Fernando Cañas

Manipular múltiples checkbox (casillas de verificación) en React es completamente diferente a su uso regular en HTML.

En este artículo veremos cómo trabajar con múltiples checkbox en React.

Aprenderás:

  • Cómo usar un checkbox como un input controlado en React.
  • Cómo usar los métodos map y reduce de los arreglos (arrays) para ejecutar cálculos complejos.
  • Cómo crear un arreglo pre-llenado de longitud específica con algunos valores específicos.

Y mucho más.

Este artículo es una parte de mi curso "Mastering Redux". Aquí puedes acceder a una previsualización de la aplicación que estaremos construyendo en el curso.

Empecemos.

Cómo trabajar con un único checkbox

Empecemos con la funcionalidad de un único checkbox antes de avanzar a múltiples checkboxes.

En este artículo usaremos la sintaxis de React Hooks para crear componentes.

Miremos el siguiente código:

<div className="App">
  Select your pizza topping:
  <div className="topping">
    <input type="checkbox" id="topping" name="topping" value="Paneer" />Paneer
  </div>
</div>

Aquí un demo en Sandbox.

En el código anterior, declaramos solamente un único checkbox de una forma muy similar a como lo haríamos en HTML.

De esta manera es muy fácil marcar y desmarcar el checkbox, tal como se muestra a continuación:

check_uncheck-1

Pero para mostrar en pantalla si la casilla se encuentra marcada o no, necesitamos convertir el checkbox en un input controlado.

En React, un input controlado es manejado por el estado, de tal manera que el valor del input solamente pueda ser cambiado si se modifica el estado relacionado con ese input.

Mira el siguiente caso:

export default function App() {
  const [isChecked, setIsChecked] = useState(false);

  const handleOnChange = () => {
    setIsChecked(!isChecked);
  };

  return (
    <div className="App">
      Select your pizza topping:
      <div className="topping">
        <input
          type="checkbox"
          id="topping"
          name="topping"
          value="Paneer"
          checked={isChecked}
          onChange={handleOnChange}
        />
        Paneer
      </div>
      <div className="result">
        Above checkbox is {isChecked ? "checked" : "un-checked"}.
      </div>
    </div>
  );
}

Aquí puedes ver el código en Sandbox.

En el código de arriba, hemos declarado el estado isChecked en el componente con un valor inicial en false haciendo uso del hook useState.

const [isChecked, setIsChecked] = useState(false);

Luego, al input de tipo checkbox le hemos dado dos props adicionales, checked y onChange:

<input
  ...
  checked={isChecked}
  onChange={handleOnChange}
/>

Cada vez que hacemos clic en el checkbox, la función handleOnChange será ejecutada. Esto hará que modifiquemos el valor del estado isChecked:

const handleOnChange = () => {
  setIsChecked(!isChecked);
};

Si el checkbox se encuentra marcado, cambiaremos el valor de isChecked a false. Pero si el checkbox se encuentra desmarcado, definiremos el estado como true usando !isChecked. Luego pasamos este valor en el input a través de la prop checked.

De esta manera el input de tipo checkbox se convierte en un input controlado cuyo valor es gestionado por el estado.

En React siempre se recomienda usar inputs controlados aun si el código se ve visualmente complicado. Esto garantiza que los cambios en los inputs solamente ocurran al interior de la función ligada al evento onChange.

El estado del input no se verá afectado de ninguna otra manera y así siempre obtendrás el valor correcto y actualizado en el input.

Solo en casos raros puedes usar React ref para usar el input de forma no controlada.

Cómo manejar múltiples checkbox

Ahora démosle una mirada a cómo manipular múltiples checkbox.

Mira el siguiente código en Sandbox:

multiple_checkboxes-2

Aquí, estamos mostrando una lista de ingredientes y su precio correspondiente. Basados en los ingredientes que sean seleccionados, necesitamos mostrar el valor total.

Previamente, con un único checkbox, solamente teníamos el estado isChecked y con ello podíamos cambiar el estado.

Sin embargo, ahora tenemos muchos checkbox, así que no es práctico agregar múltiples useState para cada uno de ellos.

Entonces, declaremos un arreglo dentro del estado indicando la marcación de cada uno de los checkbox.

Para crear un arreglo de longitud igual al número de checkbox, podemos usar el método fill de los arreglos así:

const [checkedState, setCheckedState] = useState(
    new Array(toppings.length).fill(false)
);

Aquí hemos declarado un estado con un valor inicial igual a un arreglo cuyos valores son false.

Así que si tenemos 5 ingredientes, entonces el estado checkedState será un arreglo con 5 valores en false.

[false, false, false, false, false]

Ahora, una vez que marquemos (y desmarquemos) el checkbox, cambiaremos el valor false correspondiente por true (y viceversa).

Aquí está el Sandbox final. El código App.js completo de se ve así:

import { useState } from "react";
import { toppings } from "./utils/toppings";
import "./styles.css";

const getFormattedPrice = (price) => `$${price.toFixed(2)}`;

export default function App() {
  const [checkedState, setCheckedState] = useState(
    new Array(toppings.length).fill(false)
  );

  const [total, setTotal] = useState(0);

  const handleOnChange = (position) => {
    const updatedCheckedState = checkedState.map((item, index) =>
      index === position ? !item : item
    );

    setCheckedState(updatedCheckedState);

    const totalPrice = updatedCheckedState.reduce(
      (sum, currentState, index) => {
        if (currentState === true) {
          return sum + toppings[index].price;
        }
        return sum;
      },
      0
    );

    setTotal(totalPrice);
  };

  return (
    <div className="App">
      <h3>Select Toppings</h3>
      <ul className="toppings-list">
        {toppings.map(({ name, price }, index) => {
          return (
            <li key={index}>
              <div className="toppings-list-item">
                <div className="left-section">
                  <input
                    type="checkbox"
                    id={`custom-checkbox-${index}`}
                    name={name}
                    value={name}
                    checked={checkedState[index]}
                    onChange={() => handleOnChange(index)}
                  />
                  <label htmlFor={`custom-checkbox-${index}`}>{name}</label>
                </div>
                <div className="right-section">{getFormattedPrice(price)}</div>
              </div>
            </li>
          );
        })}
        <li>
          <div className="toppings-list-item">
            <div className="left-section">Total:</div>
            <div className="right-section">{getFormattedPrice(total)}</div>
          </div>
        </li>
      </ul>
    </div>
  );
}

Entendamos lo que estamos haciendo aquí.

Hemos declarado el input tipo checkbox de la siguiente forma:

<input
  type="checkbox"
  id={`custom-checkbox-${index}`}
  name={name}
  value={name}
  checked={checkedState[index]}
  onChange={() => handleOnChange(index)}
/>

Aquí, hemos agregado el atributo checked con el correspondiente valor de true o false según lo indique el estado en checkedState.  Así cada checkbox tendrá el valor correcto para su estado de marcado.

También hemos agregado una función que maneja el evento onChange y hemos pasado el index del checkbox, de tal manera que este sea marcado o desmarcado por el método handleOnChange.

El método handleOnChange se ve de la siguiente manera:

const handleOnChange = (position) => {
  const updatedCheckedState = checkedState.map((item, index) =>
    index === position ? !item : item
  );

  setCheckedState(updatedCheckedState);

  const totalPrice = updatedCheckedState.reduce(
    (sum, currentState, index) => {
      if (currentState === true) {
        return sum + toppings[index].price;
      }
      return sum;
    },
    0
  );

  setTotal(totalPrice);
};

Aquí, estamos primero recorriendo el arreglo checkedState usando el método map de los arreglos. Si el valor del parámetro position que ha sido pasado a la función es igual al index de turno, entonces modificamos su valor. Así, si el valor es true entonces será convertido a false usando !item y viceversa.

Si el index no es igual al proveído por el parámetro position, entonces no estamos invirtiendo su valor y devolveremos su valor tal cual se encuentre:

const updatedCheckedState = checkedState.map((item, index) =>
  index === position ? !item : item
);

// the above code is the same as the below code

const updatedCheckedState = checkedState.map((item, index) => {
  if (index === position) {
    return !item;
  } else {
    return item;
  }
});

Usé el operador ternario ?: porque permite que el código sea más corto, pero tú puedes usar el método de los arreglos que prefieras.

Si no estás familiarizado con cómo funcionan los métodos como map o reduce, entonces revisa este otro artículo que escribí.

A continuación, definiremos el arreglo checkedState según lo registrado en el arreglo updatedCheckedState. Esto es importante porque si no actualizas el estado checkedState dentro de la función handleOnChange, entonces no podrás marcar o desmarcar los checkbox.

Esto es así porque estamos usando el valor de checkedState para que los checkbox puedan determinar si están marcados o desmarcados (como un input controlado, tal cual como se muestra a continuación):

<input
  type="checkbox"
  ...
  checked={checkedState[index]}
  onChange={() => handleOnChange(index)}
/>

Mira que hemos creado una variable separada updatedCheckedState y que estamos pasando esta variable a la función setCheckedState. También estamos usando el método reduce sobre updatedCheckedState y no sobre el arreglo checkedState con el estado original.

Esto es porque, por defecto, la función setCheckedState suele actualizar el estado de forma asíncrona.

Sólo porque hallas llamado a la función setCheckedState no se garantiza que vas a obtener el valor actualizado de checkedState en la siguiente línea.

Así que hemos creado una variable separada y la hemos usado para ejecutar el método reduce.

Luego, para calcular el precio total, estamos utilizando el arreglo reduce:

const totalPrice = updatedCheckedState.reduce(
  (sum, currentState, index) => {
    if (currentState === true) {
      return sum + toppings[index].price;
    }
    return sum;
  },
  0
);

El método reduce recibe cuatro parámetros, de los cuales solamente estamos usando tres: sum, currentState e index. Puedes usar nombres diferentes si quieres, ya que son simplemente parámetros.

También estamos pasando 0 como valor inicial, lo que también es conocido como el valor accumulator para el parámetro de suma sum.

Luego, dentro de la función reduce, estamos revisando si el valor actual del arreglo checkedState es true o no.

Si es true, significa que el checkbox está marcado, así que agregaremos el valor del price correspondiente usando la operación sum + toppings[index].price.

Si el valor en el arreglo checkedState es false, entonces no agregaremos su precio y devolveremos el valor previamente calculado de sum.

Así podemos asignar el valor de totalPrice al estado total usando setTotal(totalPrice).

De esta manera estamos calculando correctamente el valor total de los ingredientes seleccionados como ves a continuación:

toppings-1

Aquí hay un link para previsualizar el demo en Sandbox.

¡Gracias por leer!

Muchos desarrolladores pasan dificultades entendiendo cómo trabaja Redux. Pero todo desarrollador que use React debería tener claro cómo trabajar con Redux, ya que los proyectos en la industria usan principalmente Redux para administrar proyectos grandes.

Así que para hacértelo más fácil, he lanzando el curso Mastering Redux.

En este curso aprenderás Redux desde un nivel absolutamente principiante y construirás una food ordering app desde cero usando Redux.

Dale clic a la imagen de abajo para unirte al curso, obtener un descuento con tiempo limitado y mi libro popular Mastering Modern JavaScript gratis.

banner

¿Quieres estar actualizado con contenido regular sobre JavaScript, React y Node.js? Sígueme en LinkedIn.