Original article: How to Search and Filter Components in React

Si estás construyendo una aplicación de React, vas a querer que los usuarios puedan realizar búsquedas y obtener resultados exactos.  Y si se están obteniendo muchos elementos desde una API, entonces va a ser necesario crear una manera de que los usuarios puedan encontrar distintos elementos fácilmente.

Para este tutorial usaremos como ejemplo uno de los proyectos de API avanzados y gratuitos de Frontend Mentor.

Tabla de contenidos

  1. Preparando todo
  2. Cómo instalar React
  3. Cómo descargar la información
  4. Cómo buscar elementos en la API
  5. Cómo filtrar elementos de acuerdo con la región geográfica

Preparando todo

Para este tutorial vamos a usar la API gratuita REST COUNTRIES, provista por
Apilayer.

Básicamente, lo que haremos es extraer información de nuestro endpoint https://restcountries.eu/rest/v2/all y mostrarla de manera que sea legible para el usuario.

Luego proveeremos una forma sencilla para que los usuarios puedan buscar países específicos por sus nombres y capitales. Este es un ejemplo del resultado para un país particular:

        "name": "Colombia",
        "topLevelDomain": [".co"],
        "alpha2Code": "CO",
        "alpha3Code": "COL",
        "callingCodes": ["57"],
        "capital": "Bogotá",
        "altSpellings": ["CO", "Republic of Colombia", "República de Colombia"],
        "region": "Americas",
        "subregion": "South America",
        "population": 48759958,
        "latlng": [4.0, -72.0],
        "demonym": "Colombian",
        "area": 1141748.0,
        "gini": 55.9,
        "timezones": ["UTC-05:00"],
        "borders": ["BRA", "ECU", "PAN", "PER", "VEN"],
        "nativeName": "Colombia",
        "numericCode": "170",
        "currencies": [{
            "code": "COP",
            "name": "Colombian peso",
            "symbol": "$"
        }],
        "languages": [{
            "iso639_1": "es",
            "iso639_2": "spa",
            "name": "Spanish",
            "nativeName": "Español"
        }],
        "translations": {
            "de": "Kolumbien",
            "es": "Colombia",
            "fr": "Colombie",
            "ja": "コロンビア",
            "it": "Colombia",
            "br": "Colômbia",
            "pt": "Colômbia"
        },
        "flag": "https://restcountries.eu/data/col.svg",
        "regionalBlocs": [{
            "acronym": "PA",
            "name": "Pacific Alliance",
            "otherAcronyms": [],
            "otherNames": ["Alianza del Pacífico"]
        }, {
            "acronym": "USAN",
            "name": "Union of South American Nations",
            "otherAcronyms": ["UNASUR", "UNASUL", "UZAN"],
            "otherNames": ["Unión de Naciones Suramericanas", "União de Nações Sul-Americanas", "Unie van Zuid-Amerikaanse Naties", "South American Union"]
        }],
        "cioc": "COL"
    }]

Al terminar este tutorial con suerte habrás aprendido cómo buscar a través de una API y devolver los resultados solicitados con React.

Cómo instalar React

Usaremos create-react-app para instalar nuestro proyecto porque ofrece una configuración moderna de compilación, sin necesidad de hacer otras configuraciones.

Para instalar React, abre tu terminal (ya sea la que viene instalada con el sistema operativa o en algún editor como VS Code) y ejecuta los siguientes comandos:

npx create-react-app my-app 
cd my-app 
npm start

Si no estás seguro de cómo instalar un proyecto create-react-app de manera correcta, puedes usar la guía oficial publicada en create-react-app-dev como referencia.

En nuestro ejemplo y para mostrar los resultados de este tutorial en tiempo real, vamos a usar Codepen para instalar nuestro proyecto. Puedes hacer esto usando un modelo de Codepen creado por Lathryx:

y aquí lo tienes - hemos instalado React en Codepen.

Cómo traer la información desde nuestro API endpoint

Ahora que ya hemos instalado correctamente nuestro proyecto de React, es hora de traer la información desde nuestra API. Hay muchas formas de traer la información desde React pero dos de las más populares son Axios (un cliente HTTP basado en promesas) y Fetch API (una web API integrada en el navegador).

Vamos a usar Fetch API, provista por el navegador, y Ajax para trer la información desde nuestro API endpoint. Esto es un ejemplo usando hooks de  Ajax and APIs by React:

function MyComponent() {
      const [error, setError] = useState(null);
      const [isLoaded, setIsLoaded] = useState(false);
      const [items, setItems] = useState([]);

      // Nota: el array vacío [] significa  
      // que este useEffect correrá una sola vez,
      // parecido a componentDidMount()
      useEffect(() => {
        fetch("https://api.example.com/items")
          .then(res => res.json())
          .then(
            (result) => {
              setIsLoaded(true);
              setItems(result);
            },
            //Nota: es importante manejar los errores aquí
            //en vez de un bloque catch() para evitar tragarnos 
            // excepciones de errores en los componentes.
            (error) => {
              setIsLoaded(true);
              setError(error);
            }
          )
      }, [])

      if (error) {
        return <div>Error: {error.message}</div>;
      } else if (!isLoaded) {
        return <div>Loading...</div>;
      } else {
        return (
          <ul>
            {items.map(item => (
              <li key={item.id}>
                {item.name} {item.price}
              </li>
            ))}
          </ul>
        );
      }
    }

Este código trae información desde nuestro endpoint en la línea 10 y después usa un setState para actualizar nuestro componente cuando recibe la información.

En la línea 27 mostramos el mensaje de error si el traer información desde nuestra API falla. Si no falla, mostramos la información en una lista.

Si no estás familiarizado con las listas de React, te sugiero que le des un vistazo a esta guía para Listas React y Claves.

Ahora usemos este código para traer y mostrar la información de nuestra API REST COUNTRIES.

Para el código de ejemplo de arriba, vamos a usar import useState from React y después cambiar la linea 10 a lo siguiente:

fetch("https://restcountries.eu/rest/v2/all")

Cuando lo armamos todo junto obtenemos lo siguiente:

import { useState, useEffect } from "https://cdn.skypack.dev/react";

	// Nota: el array vacío [] significa  
      // que este useEffect correrá una sola vez.
    function App() {
        const [error, setError] = useState(null);
        const [isLoaded, setIsLoaded] = useState(false);
        const [items, setItems] = useState([]);

        useEffect(() => {
            fetch("https://restcountries.eu/rest/v2/all")
                .then((res) => res.json())
                .then(
                    (result) => {
                        setIsLoaded(true);
                        setItems(result);
                    },
   			//Nota: es importante manejar los errores aquí
            //en vez de un bloque catch() para evitar tragarnos 
            // excepciones de errores en los componentes.
                    (error) => {
                        setIsLoaded(true);
                        setError(error);
                    }
                );
        }, []);

Nota: estamos importando useState y useEffect desde "https://cdn.skypack.dev/react";. Esto es porque estamos usando una CDN para importar React en Codepen. Si instalaste React de forma local, entonces deberías usar  import { useState, useEffect } from "react";.

Ahora vamos a querer mostrar la información recibida como una lista de países. El código final debería verse así.


    // Nota: el array vacío [] significa  
      // que este useEffect correrá una sola vez,
    function App() {
        const [error, setError] = useState(null);
        const [isLoaded, setIsLoaded] = useState(false);
        const [items, setItems] = useState([]);

        useEffect(() => {
            fetch("https://restcountries.eu/rest/v2/all")
                .then((res) => res.json())
                .then(
                    (result) => {
                        setIsLoaded(true);
                        setItems(result);
                    },
   //Nota: es importante manejar los errores aquí
            //en vez de un bloque catch() para evitar tragarnos 
            // excepciones de errores en los componentes.
                    (error) => {
                        setIsLoaded(true);
                        setError(error);
                    }
                );
        }, []);

        if (error) {
            return <>{error.message}</>;
        } else if (!isLoaded) {
            return <>loading...</>;
        } else {
            return (
                /*aquí vamos a iterar sobre el elemento y mostrar cada uno como una tarjeta */
                <div className="wrapper">
                    <ul className="card-grid">
                        {items.map((item) => (
                            <li>
                                <article className="card" key={item.callingCodes}>
                                    <div className="card-image">
                                        <img src={item.flag} alt={item.name} />
                                    </div>
                                    <div className="card-content">
                                        <h2 className="card-name">{item.name}</h2>
                                        <ol className="card-list">
                                            <li>
                                                population:{" "}
                                                <span>{item.population}</span>
                                            </li>
                                            <li>
                                                Region: <span>{item.region}</span>
                                            </li>
                                            <li>
                                                Capital: <span>{item.capital}</span>
                                            </li>
                                        </ol>
                                    </div>
                                </article>
                            </li>
                        ))}
                    </ul>
                </div>
            );
        }
    }

    ReactDOM.render(<App />, document.getElementById("root"));

Aquí tienes una vista previa en Codepen:

Ahora que hemos logrado traer y mostrar la información des nuestra API REST COUNTRIES, podemos enfocarnos en buscar dentro de los países que se están mostrando.

Pero antes de hacer eso, vamos a darle estilos al ejemplo de arriba con CSS (porque se ve feo  cuando lo mostramos así).

Cuando agregamos CSS al ejemplo de arriba, obtenemos algo que debería verse como el ejemplo de abajo:

Aunque el CSS que agregamos no es perfecto, nos permite mostrar los países de una manera más prolija que antes, ¿no lo crees?

Cómo crear el componente de búsqueda

Dentro de nuestra función APP usamos el hook useState() para establecer el parámetro de consulta a una cadena de texto vacía. También tenemos el parámetro setQ el cual vamos a usar para guardar el valor que viene de nuestro formulario de búsqueda.

En la línea 13 usamos useState para definir un array con los valores por defecto que vamos a querer obtener de la API. Esto significa que vamos a poder buscar cualquier país solamente por su capital y por su nombre. Siempre se puede modificar el array en base a nuestras preferencias.

        const [error, setError] = useState(null);
        const [isLoaded, setIsLoaded] = useState(false);
        const [items, setItems] = useState([]);

        //establece el parámetro de búsqueda a una cadena de texto vacía.
        const [q, setQ] = useState("");
        //     set search parameters establece parámetros de búsqueda
        //     solo queremos buscar países por capital y nombre
        //     esta lista puede ser más larga si quieres
        //     hasta puedes buscar países por su número de población
        // 	  solo tienes que agregarlo al arreglo.
        const [searchParam] = useState(["capital", "name"]);

        useEffect(() => {
            // nuestro código para traer información
        }, []);

     }

Dentro de nuestra función de retorno vamos a crear el formulario de búsqueda. Así es cómo debería lucir nuestro código:

            return <>{error.message}</>;
        } else if (!isLoaded) {
            return <>loading...</>;
        } else {
            return (
                <div className="wrapper">
                    <div className="search-wrapper">
                        <label htmlFor="search-form">
                            <input
                                type="search"
                                name="search-form"
                                id="search-form"
                                className="search-input"
                                placeholder="Search for..."
                                value={q}
                                /*
                                // establece el valor de nuestro parametro q del useState
                                //  cada vez que el usuario tipea en el campo de búsqueda
                                */
                                onChange={(e) => setQ(e.target.value)}
                            />
                            <span className="sr-only">Search countries here</span>
                        </label>
                    </div>
                    <ul className="card-grid">
                        {items.map((item) => (
                            <li>
                                <article className="card" key={item.callingCodes}>
                                    <div className="card-image">
                                        <img src={item.flag} alt={item.name} />
                                    </div>
                                    <div className="card-content">
                                        <h2 className="card-name">{item.name}</h2>
                                        <ol className="card-list">
                                            <li>
                                                population:{" "}
                                                <span>{item.population}</span>
                                            </li>
                                            <li>
                                                Region: <span>{item.region}</span>
                                            </li>
                                            <li>
                                                Capital: <span>{item.capital}</span>
                                            </li>
                                        </ol>
                                    </div>
                                </article>
                            </li>
                        ))}
                    </ul>
                </div>
            );
        }
    }

    ReactDOM.render(<App />, document.getElementById("root"));

Ahora crearemos una función para manejar nuestra búsqueda arriba de la función de retorno (el código de arriba).

            return items.filter((item) => {
                return searchParam.some((newItem) => {
                    return (
                        item[newItem]
                            .toString()
                            .toLowerCase()
                            .indexOf(q.toLowerCase()) > -1
                    );
                });
            });
        }

Está función toma la información buscada y devuelve toda la información que se ajusta a los parámetros dentro de nuestro array searchParam si el resultado de la función indexOF() es > -1.

Ahora que ya tenemos la función creada, vamos a anidar la infomación retornada con la función de búsqueda.

{serach(items).map((item) => ( <li> // card goes here </li> ))}

Ahora la información guardada en useState() va a ser filtrada por nuestra función de búsqueda antes de que sea pasada los ítems de la lista, y de esta forma sólo mostrar los ítems que sean iguales a nuestros parámetros de búsqueda.

Aquí está el código en su totalidad y la vista previa en Codepen. Trata de usar el formulario de búsqueda de abajo para buscar cualquier país por su nombre o capital.

Cómo filtrar países por región

Ahora, podemos ir más allá y filtrar los países por la región a la que pertenecen. Por ejemplo, no queremos mostrar todos los países, solo queremos mostrar y buscar países que estén en Africa o Asia. Esto es posible con el hook useState() en React.

Regiones:

  1. África
  2. América
  3. Asia
  4. Europa
  5. Oceanía

Ahora que ya tenemos las regiones, vamos a crear un componente de filtro. Primero, vamos a establecer el useState del filtro con estos parámetros:

const [filterParam, setFilterParam] = useState(["All"]);

Como notaron, establecimos el modo predeterminado del useState en ALL a propósito, ya que queremos poder mostrar y buscar todos los países si no se especificó ninguna región.

       <select
    /*
    //aquí vamos a crear un input de selección básico
    // establecemos el valor seleccionado por defecto
    // y actualizamos el estado de setFilterParam() cada vez que se llama a onChange.
    */
      onChange={(e) => {
      setFilterParam(e.target.value);
       }}
       className="custom-select"
       aria-label="Filter Countries By Region">
        <option value="All">Filter By Region</option>
        <option value="Africa">Africa</option>
        <option value="Americas">America</option>
        <option value="Asia">Asia</option>
        <option value="Europe">Europe</option>
        <option value="Oceania">Oceania</option>
        </select>
        <span className="focus"></span>
        </div>

Ahora que ya creamos nuestro filtro, lo único que nos queda por hacer es modificar la función search. Lo que hacemos es chequear la región ingresada por el usuario y retornar solamente los países que tienen esa región:

function search(items) {
       return items.filter((item) => {
    /*
    // aquí vamos a chequear si nuestra región es igual a nuestro estado c
    // si es igual, entonces deberá retornar solo los elementos que cumplan con el criterio de búsqueda
    // caso contrario, deberá retornar todos los países.
    */
       if (item.region == filterParam) {
           return searchParam.some((newItem) => {
             return (
               item[newItem]
                   .toString()
                   .toLowerCase()
                   .indexOf(q.toLowerCase()) > -1
                        );
                    });
                } else if (filterParam == "All") {
                    return searchParam.some((newItem) => {
                        return (
                            item[newItem]
                                .toString()
                                .toLowerCase()
                                .indexOf(q.toLowerCase()) > -1
                        );
                    });
                }
            });
        }

Puedes encontrar el código completo y la vista previa en Codepen. Trata de filtrar los países y observa los resultados.

Una vez que agreguemos un poco de CSS vamos a poder ver la vista previa final de nuestra aplicación de React.

A modo de conclusión

Cuando estás trabajando con una gran cantidad de información y necesitas mostrársela al usuario, las funciones de búsqueda y filtrado ayudan a navegar y encontrar la información rápidamente.

Si tienes preguntas, puedes contactarme y estaré más que feliz de conversar.

Puedes encontrar la vista previa completa de este proyecto en earthly vercel app y puedes seguirme en X @sprucekhalifa.