Artículo original escrito por Yogesh Chavan.
Artículo original How to Create a Truly Reusable React Component from Scratch.
Traducido y adaptado por Josue Custodio.

En este tutorial, creará una aplicación con React.  Y aprenderá a crear un componente de autosugestión verdaderamente reutilizable desde cero.

Esta aplicación permitirá al usuario buscar un país en una lista de países. Mostrará sugerencias coincidentes debajo del campo de entrada para el país que el usuario ha ingresado.

Al crear esta aplicación, aprenderá:

  • Cómo crear un componente reutilizable.
  • Cómo utilizar el useRef hook para administrar sugerencias automáticas.
  • Cómo crear un hook reutilizable personalizado.
  • Cómo realizar la búsqueda de manera eficiente.

y mucho más.

Puede encontrar la demostración en vivo de la aplicación final aquí.

A continuación se muestra la demostración funcional de la función de sugerencias automáticas.

suggestion_demo

Así que comencemos a construir la aplicación.

Configurar el proyecto

Usaremos create-react-app para inicializar el proyecto.

Usaremos la sintaxis de React Hooks para crear los componentes.

Crea un nuevo proyecto de React ejecutando el siguiente comando:

‌                   npx create-react-app react-autosuggestion-app              

Una vez que hayas creado el proyecto, elimina todos los archivos de la carpeta src y crea los archivos  index.js, App.js, styles.css dentro de la carpeta  src.

También crea las carpetas components y custom-hooks dentro de la carpeta  src.

Instala las dependencias necesarias ejecutando el siguiente comando desde la terminal o el símbolo del sistema:            

yarn add axios@0.21.1 lodash@4.17.21 react-bootstrap@1.6.1 bootstrap@5.1.0

Una vez instalados, abre el archivo src/styles.css y agrega el contenido de este archivo dentro de él.

Cómo construir las páginas iniciales

Crea un nuevo archivo countries.json dentro de la carpeta public  y agrega el contenido de este archivo dentro de ella.

Crea un archivo AutoComplete.js dentro de la carpeta components  con el siguiente código:  

import React from 'react';

function AutoComplete({ isVisible, suggestions, handleSuggestionClick }) {
  return (
    <div className={`${isVisible ? 'show suggestion-box' : 'suggestion-box'}`}>
      <ul>
        {suggestions.map((country, index) => (
          <li key={index} onClick={() => handleSuggestionClick(country)}>
            {country}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default AutoComplete;

En este archivo, mostramos las sugerencias al usuario una vez que el usuario escribe algo en el cuadro de entrada de texto.

Crea un archivo useOutsideClick.js dentro de la carpeta custom-hooks  con el siguiente código:

import { useState, useRef, useEffect } from 'react';

const useOutsideClick = () => {
  const [isVisible, setIsVisible] = useState(false);
  const ref = useRef();

  const handleOutsideClick = () => {
    if (ref.current) {
      setIsVisible(false);
    }
  };

  useEffect(() => {
    document.addEventListener('click', handleOutsideClick);
    return () => {
      document.removeEventListener('click', handleOutsideClick);
    };
  }, []);

  return [ref, isVisible, setIsVisible];
};

export default useOutsideClick;

Aquí, hemos creado un hook personalizado que mostrará/ocultará el cuadro de sugerencias.

Inicialmente, hemos declarado un estado para ocultar el cuadro de sugerencias estableciendo el valor en false:

const [isVisible, setIsVisible] = useState(false);

Entonces hemos declarado un ref:            

const ref = useRef();

Estamos devolviendo este ref de nuestro hook personalizado junto con el isVisible y setIsVisible:

return [ref, isVisible, setIsVisible];

Entonces, dentro del componente donde sea que usemos el hook useOutsideClick, podemos usar esta ref para asignarla al buzón de sugerencias. Entonces, si hay varios campos de entrada, cada campo de entrada tendrá su propio cuadro de sugerencias y ocultará y mostrará la funcionalidad.

Dentro de la función handleOutsideClick, tenemos el siguiente código:

const handleOutsideClick = () => {
  if (ref.current) {
    setIsVisible(false);
  }
};

Aquí, estamos comprobando ref.current porque queremos llamar a la función setIsVisible solo si la ref del buzón de sugerencias está disponible y no cada vez que hacemos clic en la página.

Luego, hemos agregado manejador de eventos para llamar a la función handleOutsideClick:

useEffect(() => {
  document.addEventListener('click', handleOutsideClick);
  return () => {
    document.removeEventListener('click', handleOutsideClick);
  };
}, []);

También eliminaremos el manejador de eventos devolviendo una función del hook useEffect una vez que el componente está desmontado.

Cómo crear un componente React reutilizable

Ahora, crea un archivo InputControl.js dentro de la carpeta components  con el siguiente código:

/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import _ from 'lodash';
import { Form } from 'react-bootstrap';
import AutoComplete from './AutoComplete';
import useOutsideClick from '../custom-hooks/useOutsideClick';

const InputControl = ({ name, label, placeholder }) => {
  const [documentRef, isVisible, setIsVisible] = useOutsideClick();
  const [suggestions, setSuggestions] = useState([]);
  const [selectedCountry, setSelectedCountry] = useState('');
  const [searchTerm, setSearchTerm] = useState('');
  const [errorMsg, setErrorMsg] = useState('');
  const ref = useRef();

  useEffect(() => {
    ref.current = _.debounce(processRequest, 300);
  }, []);

  function processRequest(searchValue) {
    axios
      .get('/countries.json')
      .then((response) => {
        const countries = response.data;
        const result = countries.filter((country) =>
          country.toLowerCase().includes(searchValue.toLowerCase())
        );
        setSuggestions(result);
        if (result.length > 0) {
          setIsVisible(true);
        } else {
          setIsVisible(false);
        }
        setErrorMsg('');
      })
      .catch(() => setErrorMsg('Something went wrong. Try again later'));
  }

  function handleSearch(event) {
    event.preventDefault();
    const { value } = event.target;
    setSearchTerm(value);
    ref.current(value);
  }

  function handleSuggestionClick(countryValue) {
    setSelectedCountry(countryValue);
    setIsVisible(false);
  }

  return (
    <Form.Group controlId="searchTerm">
      <Form.Label>{label}</Form.Label>
      <Form.Control
        className="input-control"
        type="text"
        value={searchTerm}
        name={name}
        onChange={handleSearch}
        autoComplete="off"
        placeholder={placeholder}
      />
      <div ref={documentRef}>
        {isVisible && (
          <AutoComplete
            isVisible={isVisible}
            suggestions={suggestions}
            handleSuggestionClick={handleSuggestionClick}
          />
        )}
      </div>
      {selectedCountry && (
        <div className="selected-country">
          Your selected country: {selectedCountry}
        </div>
      )}
      {errorMsg && <p className="errorMsg">{errorMsg}</p>}
    </Form.Group>
  );
};

export default InputControl;

En este archivo, hemos creado un componente reutilizable con búsquedas y sugerencias disponibles en el componente.

Inicialmente, estamos haciendo referencia al hook useOutsideClick:

const [documentRef, isVisible, setIsVisible] = useOutsideClick();

Estamos almacenando la ref devuelta por el hook en la variable documentRef.‌‌‌ Cada vez que un usuario escribe algo en el cuadro de texto, hacemos una llamada a la API para obtener una lista de países con los criterios de búsqueda coincidentes.

Pero para evitar las llamadas API innecesarias en cada carácter ingresado en el cuadro de texto, usaremos el método antirrebote de la biblioteca lodash.  Nos permite llamar a la API solo después de que hayan pasado 300 milisegundos una vez que el usuario haya dejado de escribir usando el siguiente código:

ref.current = _.debounce(processRequest, 300);

La función _.debounce llamada devuelve una función que hemos almacenado en la variable ref.current. Llamaremos a la función almacenada allí una vez que hayan pasado 300 milisegundos.

Estamos usando ref en lugar de una variable normal porque necesitamos que esta inicialización ocurra solo una vez cuando el componente está montado.  El valor de la variable normal se perderá en cada nueva renderización del componente cuando cambie algún estado o propiedad.

Estamos llamando a la función almacenada en ref.current desde la función handleSearch pasando el valor introducido por el usuario.

Entonces, una vez que llamamos a la función almacenada en ref.current, la función  processRequest será llamada detrás de escena.

La función processRequest recibirá automáticamente el valor pasado a la función ref.current.

Dentro de la función processRequest, hacemos una llamada API para obtener la lista de países.

function processRequest(searchValue) {
  axios
    .get('/countries.json')
    .then((response) => {
      const countries = response.data;
      const result = countries.filter((country) =>
        country.toLowerCase().includes(searchValue.toLowerCase())
      );
      setSuggestions(result);
      if (result.length > 0) {
        setIsVisible(true);
      } else {
        setIsVisible(false);
      }
      setErrorMsg('');
    })
    .catch(() => setErrorMsg('Something went wrong. Try again later'));
}

Aquí, una vez que tenemos la respuesta de la API, usamos el método de filtro del arreglo para filtrar solo los países que coinciden con el término de búsqueda proporcionado.

Luego, establecemos la lista de países en el estado de sugerencias usando setSuggestions(result).

A continuación, comprobamos la longitud del arreglo de resultados para mostrar u ocultar el cuadro de sugerencias.

Si verificas el JSX que devuelve el componente, se ve así:

return (
  <Form.Group controlId="searchTerm">
    <Form.Label>{label}</Form.Label>
    <Form.Control
      className="input-control"
      type="text"
      value={searchTerm}
      name={name}
      onChange={handleSearch}
      autoComplete="off"
      placeholder={placeholder}
    />
    <div ref={documentRef}>
      {isVisible && (
        <AutoComplete
          isVisible={isVisible}
          suggestions={suggestions}
          handleSuggestionClick={handleSuggestionClick}
        />
      )}
    </div>
    {selectedCountry && (
      <div className="selected-country">
        Your selected country: {selectedCountry}
      </div>
    )}
    {errorMsg && <p className="errorMsg">{errorMsg}</p>}
  </Form.Group>
);

Aquí, para el cuadro de texto de entrada, hemos agregado un manejador handleSearch onChange que se ve así:

function handleSearch(event) {
  event.preventDefault();
  const { value } = event.target;
  setSearchTerm(value);
  ref.current(value);
}

Actualizamos el estado de searchTerm con el valor escrito por el usuario. Entonces llamamos a la función almacenada en el  ref.current pasándole el valor que ingresa el usuario.

Llamar a ref.current internamente llama a la función processRequest donde en realidad llamamos a la API.

Luego, después del cuadro de texto de entrada, hemos agregado un div con la ref para mostrar las sugerencias:

<div ref={documentRef}>
  {isVisible && (
    <AutoComplete
      isVisible={isVisible}
      suggestions={suggestions}
      handleSuggestionClick={handleSuggestionClick}
    />
  )}
</div>

Solo mostramos sugerencias si isVisible es true, lo que sucede cuando obtenemos resultados de la API dentro de la función  processRequest.

Aquí, pasamos las sugerencias para que se muestren en el componente AutoComplete.‌‌ Una vez que hagamos clic en cualquiera de las sugerencias, la función handleSuggestionClick se ejecuta que está actualizando el selectedCountry y escondiendo las sugerencias:

function handleSuggestionClick(countryValue) {
  setSelectedCountry(countryValue);
  setIsVisible(false);
}

Cómo utilizar el componente reutilizable

Ahora, abre el archivo App.js y agrega el siguiente código dentro de él:

import React from 'react';
import { Form } from 'react-bootstrap';
import InputControl from './components/InputControl';

const App = () => {
  return (
    <div className="main">
      <h1>React AutoSuggestion Demo</h1>
      <div className="search-form">
        <Form>
          <InputControl
            name="country"
            label="Enter Country"
            placeholder="Type a country name"
          />
        </Form>
      </div>
    </div>
  );
};

export default App;

Ahora, inicia la aplicación ejecutando el siguiente comando desde la terminal o el símbolo del sistema:

yarn start

   

2

Como puedes ver, una vez que selecciona cualquier valor de la sugerencia, el valor seleccionado se muestra debajo del cuadro de texto.

Nota: hemos creado un componente InputControl separado que muestra el campo de entrada junto con su cuadro de sugerencias.

Entonces podemos reutilizar el mismo componente InputControl nuevamente para mostrar sugerencias en otro cuadro de texto de entrada como se muestra a continuación:

import React from 'react';
import { Form } from 'react-bootstrap';
import InputControl from './components/InputControl';

const App = () => {
  return (
    <div className="main">
      <h1>React AutoSuggestion Demo</h1>
      <div className="search-form">
        <Form>
          <InputControl
            name="country"
            label="Enter Country"
            placeholder="Type a country name"
          />
          <InputControl
            name="country"
            label="Enter Country"
            placeholder="Type a country name"
          />
        </Form>
      </div>
    </div>
  );
};

export default App;

‌                                      

3

Como puedes ver, hemos agregado otro componente InputControl  para el país, por lo que podemos manejar la sugerencia para cada cuadro de texto de entrada por separado.

Entonces, si deseas mostrar diferentes sugerencias para otro cuadro de texto, puedes pasar un accesorio adicional al componente InputControl y con base en ese apoyo, muestre diferentes resultados en el buzón de sugerencias.

Conclusión

Como hemos visto en este tutorial, al crear un componente reutilizable InputControl y usando ref para administrar la sugerencia de cada cuadro de texto de entrada por separado, podemos crear un componente realmente reutilizable para mostrar sugerencias de autocompletar.

Puedes encontrar el código fuente completo para este tutorial en este repositorio y la demostración en vivo aquí.

¡Gracias por leer!

¿Quiere mantenerse actualizado con el contenido regular sobre JavaScript, React, Node.js? Sígueme en LinkedIn.