Guía: Crea un componente de selección (dropdown) en React

Los componentes de selección desplegables o dropdown selector, son uno de los componentes más usados en los formularios.

Si bien, este tipo de elementos ya están implementados de forma nativa con las etiquetas HTML <select></select>, lo cierto es que para mejorar la experiencia de uso y la estética de las aplicaciones, es común crear con el componente por separado.

Por esa razón el día de hoy te mostraré como crear un componente dropdown con React de forma sencilla.

1. Crear el esqueleto del componente

Lo primero que se debe hacer es crear el componente. La estructura básica es la siguiente:

import React, { useState, useRef } from "react";

const Dropdown = () => {
  const [open, setOpen] = useState(false);
	const [list-wrapper, setSelected] = useState(null);

  const toggle = (e) => {
    setOpen(!open);
  };

  return (
    <div>
      <div className="button-box" onClick={toggle} ref={buttonRef}>
			   Click here!
      </div>
      <div div className="list-wrapper"  ref={menuRef}>
        {open && (
          <ul div className="list" >
            <li>This is a new element</li>
            <li>This is a new element</li>
            <li>This is a new element</li>
          </ul>
        )}
      </div>
    </div>
  );
};

Como se ve, hay dos componentes básicos: el botón button-box y la lista de selección list-wrapper.

En cuanto a funcionalidad básica, se agregó un estado llamado open en el que se define la lógica de abrir y cerrar la caja de diálogo. Otro estado básico es el de selected, que básicamente almacenará el elemento seleccionado.

2. Agregar lista de elementos

Para que puedas usar este componente en cualquier parte del código, se agregó un parámetro al componente llamado options, donde estarán los elementos de la caja se selección.

const Dropdown = ({options}) => {
/*
	Código del componente
*/
}

Para efectos prácticos, options solo es un array de string, por lo que necesitaremos agregarle un ID a cada elemento. Así que creamos otro estado llamado elementList.

También es necesario almacenar el valor actual seleccionado, por lo que se creará también un estado llamado selected. Para darle funcionalidad, creamos una variable que manejará los cambios; esta se llamará handleOptionClick:

import React, { useState, useRef } from "react";

const Dropdown = ({options, onChange}) => {
  const [open, setOpen] = useState(false);
	const [list-wrapper, setSelected] = useState(null);

	const [elementList, setElementList] = useState(
	    options.map((option) => ({
	      id: Date.now()-Math.random()+ "select",
	      text: option,
	    }))
	  );

	const [select, setSelect] = useState(elementList[0]);
  
const toggle = (e) => {
    setOpen(!open);
  };
	
	const handleOptionClick = (option) => {
    setSelect(option);
    setOpen(false);
  };
	
  return (
    <div>
      <div className="button-box" onClick={toggle}>
			   {select.text ? select.text : "Click here!"} 
     </div>
      <div div className="list-wrapper">
        {open && (
          <ul div className="list" >
							{
							elementList.map((option) => (
	              <li key={option.id} onClick={() => handleOptionClick(option)} className={option.id == select.id ? 'selected' : ''}>
	                {option.text}
	              </li>))
								}
          </ul>
        )}
      </div>
    </div>
  );
};

3. Crear estilos

Probablemente una de las partes más importantes de este tipo de componentes son los estilos. Por eso lo siguiente es crear el código CSS que tendrá nuestra caja de diálogos.

Es importante recalcar que los estilos se pueden agregar de distintas maneras. Por ejemplo, con css común, sass, módulos css, styled componets, etc. O también se pueden definir utilizando frameworks como tailwind.

Para este ejemplo usaré sass (SCSS ).

.button-box {
	cursor: pointer;
  font-size: 14px;
  border-radius: 5px;
  font-weight: 700;
  border: 1px solid #dedede;
  padding: 8px 20px;
  background-color: transparent;
  width: fit-content;
  display: flex;
  align-items: center;
  gap: 5px;
}

.list {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  background-color: var(--bg);
  font-size: 14px;
  border-radius: 5px;
  border: #e6e6e6 1px solid;
  padding: 0;
  margin-top: 4px;
  
	li {
    top: 0;
    list-style: none;
    cursor: pointer;
    padding: 8px 10px;
    padding-left: 25px;
  }

  li:hover {
    background-color: rgba(204, 204, 204, 0.6);
  }

  .selected { 
    background-color: rgba(204, 204, 204, 0.3);
    padding-left: 0;
  }

  .checkIcon {
    width: 12px;
    margin-right: 5px;
    margin-left: 8px;
  }
}

4. Agregar funcionalidades extras

Cerrar al hacer click afuera

Uno de los comportamientos más comunes cuando usas uno de estos elementos es que se cierran si se hace click afuera de la caja de selección.

Para esto hay muchas formas de hacerlo, pero para seguir las buenas prácticas, declararemos un custome hook llamado useClickOutside()

import React, { useEffect } from "react";

const useClickOutside = (ref, onClickOutside, buttonRef) => {
  useEffect(() => {
    function handleClickOutside(event) {
      if (buttonRef?.current && buttonRef?.current.contains(event.target)) {
        return;
      }
      if (ref.current && !ref.current.contains(event.target)) {
        onClickOutside();
      }
    }
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [ref, onClickOutside]);
};

export default useClickOutside;

Ahora para implementarlo, lo primero que es necesario hacer es importar el hook useRef y asignarlo en el componente. Esto permitirá identificar si se realiza un click fuera de la caja.

Luego de que ya se tiene definido el ref, se invoca el hook. Para esto se tiene que pasar los dos parámetros: 1) El callback o función que se ejecutará al hacer click afuera 2) La referencia del contenedor

import React, { useState, useRef } from "react";
import useClickOutside from "./hooks/useClickOutside";

const Dropdown = ({options}) => {
 /* Código JS del componente */
	const menuRef = useRef(null);

  useClickOutside(menuRef, () => setOpen(false), buttonRef);

	// ... Código JSX
	return (
		<div>
			{ ... }
      <div div className="list-wrapper"  ref={menuRef}>
					{...}
			</div>
		</div>
	)

}

Resultado final

Luego de agregar todas las funcionalidades y estados, el componente final quedaría de la siguiente manera.

import React, { useState, useRef } from "react";
import useClickOutside from "./hooks/useClickOutside";

const Dropdown = ({options, onChange}) => {
  const [open, setOpen] = useState(false);
	const [list-wrapper, setSelected] = useState(null);

	const [elementList, setElementList] = useState(
	    options.map((option) => ({
	      id: Date.now()-Math.random()+ "select",
	      text: option,
	    }))
	  );

	const [select, setSelect] = useState(elementList[0]);

	const menuRef = useRef(null);
  const buttonRef = useRef(null);

	useClickOutside(menuRef, () => setOpen(false), buttonRef);
  
	const toggle = (e) => {
    setOpen(!open);
  };
	
const handleOptionClick = (option) => {
    setSelect(option);
    setOpen(false);
    onChange(option.text);
  };
	
  return (
    <div>
      <div className="button-box" onClick={toggle} ref={buttonRef}>
			   {select.text ? select.text : "Click here!"} 
     </div>
      <div div className="list-wrapper"  ref={menuRef}>
        {open && (
          <ul div className="list" >
							{
							elementList.map((option) => (
	              <li key={option.id} onClick={() => handleOptionClick(option)} className={option.id == select.id ? 'selected' : ''}>
	                {option.text}
	              </li>))
								}
          </ul>
        )}
      </div>
    </div>
  );
};

Es importante tener en cuenta que este dropdown es bastante genérico, por lo que se puede usar en casi cualquier situación. Aun así si deseas personalizarlo, no olvides revisar el repositorio de GitHub.

TaggedGuía de ReactTutorial