CÓMO IMPLEMENTAR BÚSQUEDA Y FILTRADO EN REACT
Introducción a la búsqueda y filtrado en React
En el desarrollo de aplicaciones web modernas, implementar funcionalidades como búsqueda en tiempo real, filtrado dinámico y paginación es esencial para mejorar la experiencia del usuario. Este tutorial te guiará paso a paso para crear un componente en React que permita buscar, filtrar y paginar datos obtenidos de una API externa. Utilizaremos la API de CountryAPI.io para obtener información de países, mostrando cómo integrar estas funcionalidades en una aplicación React. Para seguir este tutorial, necesitarás conocimientos básicos de React, JavaScript y el uso de APIs REST. Además, asumimos que estás familiarizado con conceptos como el manejo de estados y el uso de hooks como useState y useEffect.
El objetivo es construir una aplicación que muestre una lista de países, permita buscar por nombre o cualquier atributo, filtrar por región y paginar los resultados para optimizar el rendimiento. Todos los ejemplos incluirán código funcional y estarán acompañados de explicaciones detalladas para que puedas adaptarlos a tus propios proyectos.
Configuración inicial del proyecto
Para comenzar, crea una nueva aplicación React utilizando Create React App. Este paquete configura un entorno de desarrollo con todas las dependencias necesarias. Abre tu terminal y ejecuta los siguientes comandos:
npx create-react-app buscador-paises
cd buscador-paises
npm start
Este proceso genera una estructura básica de proyecto y abre una vista previa en tu navegador en http://localhost:3000. Asegúrate de tener Node.js instalado en tu sistema antes de ejecutar estos comandos.
Necesitarás una clave de API para acceder a CountryAPI.io. Regístrate en su sitio web oficial y obtén tu clave desde el panel de control. Esta clave será necesaria para realizar solicitudes a la API.
Obtención de datos desde la API
El primer paso es realizar una solicitud GET al endpoint https://countryapi.io/api/all para obtener los datos de los países. Utilizaremos el hook useEffect para ejecutar esta solicitud al cargar el componente y el hook useState para manejar el estado de los datos, errores y el estado de carga.
En el archivo src/App.js, reemplaza el contenido por el siguiente código:
import { useState, useEffect } from "react";
import "./App.css";
function App() {
const [error, setError] = useState(null);
const [loaded, setLoaded] = useState(false);
const [items, setItems] = useState([]);
useEffect(() => {
const request_headers = new Headers();
const api_key = "TU_CLAVE_DE_API_AQUÍ";
request_headers.append("Authorization", `Bearer ${api_key}`);
request_headers.append("Content-Type", "application/json");
const request_options = {
method: "GET",
headers: request_headers,
};
fetch("https://countryapi.io/api/all", request_options)
.then((res) => res.json())
.then(
(result) => {
setLoaded(true);
setItems(result);
},
(error) => {
setLoaded(true);
setError(error);
}
);
}, []);
if (error) {
return <div>{error.message}</div>;
} else if (!loaded) {
return <div>Cargando...</div>;
} else {
return <div>¡Datos cargados!</div>;
}
}
export default App;
En este código, configuramos una solicitud HTTP utilizando la API Fetch de JavaScript. La clave de API se incluye en los encabezados de la solicitud para autenticar el acceso. Los datos devueltos se almacenan en el estado items, mientras que error y loaded controlan los estados de error y carga, respectivamente. El hook useEffect asegura que la solicitud se realice solo una vez al montar el componente, gracias a la dependencia vacía [].
Mostrar los datos en una lista
Una vez obtenidos los datos, necesitamos mostrarlos en una lista de tarjetas que representen cada país. La API devuelve un objeto, pero para iterar sobre los países, convertiremos sus valores en un arreglo usando Object.values.
Actualiza el archivo src/App.js con el siguiente código:
import { useState, useEffect } from "react";
import "./App.css";
function App() {
const [error, setError] = useState(null);
const [loaded, setLoaded] = useState(false);
const [items, setItems] = useState([]);
useEffect(() => {
const request_headers = new Headers();
const api_key = "TU_CLAVE_DE_API_AQUÍ";
request_headers.append("Authorization", `Bearer ${api_key}`);
request_headers.append("Content-Type", "application/json");
const request_options = {
method: "GET",
headers: request_headers,
};
fetch("https://countryapi.io/api/all", request_options)
.then((res) => res.json())
.then(
(result) => {
setLoaded(true);
setItems(result);
},
(error) => {
setLoaded(true);
setError(error);
}
);
}, []);
const data = Object.values(items);
if (error) {
return <div>{error.message}</div>;
} else if (!loaded) {
return <div>Cargando...</div>;
} else {
return (
<div className="wrapper">
<ul className="card-grid">
{data.map((item) => (
<li key={item.alpha3Code}>
<article className="card">
<div className="card-image">
<img
src={item.flag?.large}
alt={item.name}
/>
</div>
<div className="card-content">
<h2 className="card-name">{item.name}</h2>
<p>Capital: {item.capital}</p>
<p>Región: {item.region}</p>
</div>
</article>
</li>
))}
</ul>
</div>
);
}
}
export default App;
También necesitarás algo de CSS para estilizar las tarjetas. Crea o edita el archivo src/App.css:
.wrapper {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
list-style: none;
padding: 0;
}
.card {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.card-image img {
width: 100%;
height: 150px;
object-fit: cover;
}
.card-content {
padding: 15px;
}
.card-name {
font-size: 1.5em;
margin: 0 0 10px;
}
Este código genera una cuadrícula de tarjetas, cada una mostrando la bandera, el nombre, la capital y la región del país. La conversión de los datos a un arreglo con Object.values permite iterar fácilmente con el método map. La clave única key={item.alpha3Code} asegura un rendimiento óptimo al renderizar la lista.
Implementar la funcionalidad de búsqueda
Ahora agregaremos un campo de búsqueda para permitir a los usuarios buscar países por cualquier atributo, como el nombre o la capital. Usaremos el hook useState para almacenar el término de búsqueda y filtraremos los datos dinámicamente según la entrada del usuario.
Actualiza src/App.js con el siguiente código:
import { useState, useEffect } from "react";
import "./App.css";
function App() {
const [error, setError] = useState(null);
const [loaded, setLoaded] = useState(false);
const [items, setItems] = useState([]);
const [query, setQuery] = useState("");
useEffect(() => {
const request_headers = new Headers();
const api_key = "TU_CLAVE_DE_API_AQUÍ";
request_headers.append("Authorization", `Bearer ${api_key}`);
request_headers.append("Content-Type", "application/json");
const request_options = {
method: "GET",
headers: request_headers,
};
fetch("https://countryapi.io/api/all", request_options)
.then((res) => res.json())
.then(
(result) => {
setLoaded(true);
setItems(result);
},
(error) => {
setLoaded(true);
setError(error);
}
);
}, []);
const data = Object.values(items);
const search_parameters = Object.keys(Object.assign({}, ...data));
function search(items) {
return items.filter((item) =>
search_parameters.some((parameter) =>
item[parameter]
?.toString()
.toLowerCase()
.includes(query.toLowerCase())
)
);
}
if (error) {
return <div>{error.message}</div>;
} else if (!loaded) {
return <div>Cargando...</div>;
} 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="Buscar países..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<span className="sr-only">Buscar países aquí</span>
</label>
</div>
<ul className="card-grid">
{search(data).map((item) => (
<li key={item.alpha3Code}>
<article className="card">
<div className="card-image">
<img
src={item.flag?.large}
alt={item.name}
/>
</div>
<div className="card-content">
<h2 className="card-name">{item.name}</h2>
<p>Capital: {item.capital}</p>
<p>Región: {item.region}</p>
</div>
</article>
</li>
))}
</ul>
</div>
);
}
}
export default App;
Agrega los siguientes estilos a src/App.css:
.search-wrapper {
margin-bottom: 20px;
}
.search-input {
width: 100%;
padding: 10px;
font-size: 1em;
border: 1px solid #ddd;
border-radius: 4px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
Aquí, añadimos un campo de entrada que actualiza el estado query cada vez que el usuario escribe. La función search filtra los datos verificando si alguna propiedad del objeto (obtenida dinámicamente con Object.keys) incluye el término de búsqueda. Esto hace que la búsqueda sea flexible y adaptable a cambios en la estructura de los datos de la API.
Crear un componente de filtrado
Además de buscar, queremos permitir a los usuarios filtrar los países por región. En lugar de codificar las regiones manualmente, las obtendremos dinámicamente de los datos para garantizar que el componente sea robusto frente a cambios en la API.
Actualiza src/App.js:
import { useState, useEffect } from "react";
import "./App.css";
function App() {
const [error, setError] = useState(null);
const [loaded, setLoaded] = useState(false);
const [items, setItems] = useState([]);
const [query, setQuery] = useState("");
const [filter, setFilter] = useState("");
useEffect(() => {
const request_headers = new Headers();
const api_key = "TU_CLAVE_DE_API_AQUÍ";
request_headers.append("Authorization", `Bearer ${api_key}`);
request_headers.append("Content-Type", "application/json");
const request_options = {
method: "GET",
headers: request_headers,
};
fetch("https://countryapi.io/api/all", request_options)
.then((res) => res.json())
.then(
(result) => {
setLoaded(true);
setItems(result);
},
(error) => {
setLoaded(true);
setError(error);
}
);
}, []);
const data = Object.values(items);
const search_parameters = Object.keys(Object.assign({}, ...data));
const filter_items = [...new Set(data.map((item) => item.region))];
function search(items) {
return items.filter(
(item) =>
item.region.includes(filter) &&
search_parameters.some((parameter) =>
item[parameter]
?.toString()
.toLowerCase()
.includes(query.toLowerCase())
)
);
}
if (error) {
return <div>{error.message}</div>;
} else if (!loaded) {
return <div>Cargando...</div>;
} 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="Buscar países..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<span className="sr-only">Buscar países aquí</span>
</label>
</div>
<div className="select">
<select
onChange={(e) => setFilter(e.target.value)}
className="custom-select"
aria-label="Filtrar países por región"
>
<option value="">Filtrar por región</option>
{filter_items.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</div>
<ul className="card-grid">
{search(data).map((item) => (
<li key={item.alpha3Code}>
<article className="card">
<div className="card-image">
<img
src={item.flag?.large}
alt={item.name}
/>
</div>
<div className="card-content">
<h2 className="card-name">{item.name}</h2>
<p>Capital: {item.capital}</p>
<p>Región: {item.region}</p>
</div>
</article>
</li>
))}
</ul>
</div>
);
}
}
export default App;
Añade estos estilos a src/App.css:
.select {
margin-bottom: 20px;
}
.custom-select {
padding: 10px;
font-size: 1em;
border: 1px solid #ddd;
border-radius: 4px;
width: 100%;
max-width: 300px;
}
La variable filter_items utiliza Set para obtener una lista única de regiones. El componente select permite al usuario elegir una región, y el estado filter se actualiza con el valor seleccionado. La función search ahora combina el filtrado por región con la búsqueda, asegurando que los resultados cumplan ambas condiciones.
Implementar paginación
La paginación mejora el rendimiento al limitar la cantidad de elementos mostrados en la pantalla. Implementaremos una paginación simple que muestra un número fijo de países y permite cargar más con un botón.
Actualiza src/App.js:
import { useState, useEffect } from "react";
import "./App.css";
function App() {
const [error, setError] = useState(null);
const [loaded, setLoaded] = useState(false);
const [items, setItems] = useState([]);
const [query, setQuery] = useState("");
const [filter, setFilter] = useState("");
const [paginate, setPaginate] = useState(8);
useEffect(() => {
const request_headers = new Headers();
const api_key = "TU_CLAVE_DE_API_AQUÍ";
request_headers.append("Authorization", `Bearer ${api_key}`);
request_headers.append("Content-Type", "application/json");
const request_options = {
method: "GET",
headers: request_headers,
};
fetch("https://countryapi.io/api/all", request_options)
.then((res) => res.json())
.then(
(result) => {
setLoaded(true);
setItems(result);
},
(error) => {
setLoaded(true);
setError(error);
}
);
}, []);
const data = Object.values(items);
const search_parameters = Object.keys(Object.assign({}, ...data));
const filter_items = [...new Set(data.map((item) => item.region))];
function search(items) {
return items.filter(
(item) =>
item.region.includes(filter) &&
search_parameters.some((parameter) =>
item[parameter]
?.toString()
.toLowerCase()
.includes(query.toLowerCase())
)
);
}
const loadMore = () => {
setPaginate((prevValue) => prevValue + 8);
};
if (error) {
return <div>{error.message}</div>;
} else if (!loaded) {
return <div>Cargando...</div>;
} 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="Buscar países..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<span className="sr-only">Buscar países aquí</span>
</label>
</div>
<div className="select">
<select
onChange={(e) => setFilter(e.target.value)}
className="custom-select"
aria-label="Filtrar países por región"
>
<option value="">Filtrar por región</option>
{filter_items.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</div>
<ul className="card-grid">
{search(data)
.slice(0, paginate)
.map((item) => (
<li key={item.alpha3Code}>
<article className="card">
<div className="card-image">
<img
src={item.flag?.large}
alt={item.name}
/>
</div>
<div className="card-content">
<h2 className="card-name">
{item.name}
</h2>
<p>Capital: {item.capital}</p>
<p>Región: {item.region}</p>
</div>
</article>
</li>
))}
</ul>
{search(data).length > paginate && (
<button className="load-more" onClick={loadMore}>
Cargar más
</button>
)}
</div>
);
}
}
export default App;
Añade este estilo a src/App.css:
.load-more {
display: block;
margin: 20px auto;
padding: 10px 20px;
font-size: 1em;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.load-more:hover {
background-color: #0056b3;
}
El estado paginate controla cuántos elementos se muestran, comenzando con 8. La función loadMore incrementa este valor en 8 cada vez que se hace clic en el botón “Cargar más”. El método slice(0, paginate) limita los elementos renderizados, y el botón solo aparece si hay más elementos por mostrar. Esto mejora el rendimiento de la aplicación al reducir la carga inicial.
Conclusiones
En este tutorial, hemos construido una aplicación React que integra búsqueda, filtrado y paginación utilizando datos de la API de CountryAPI.io. Aprendimos a obtener datos dinámicamente, mostrarlos en una interfaz de usuario, implementar una búsqueda flexible basada en múltiples parámetros, filtrar por región de manera dinámica y optimizar el rendimiento con paginación. Estas técnicas son fundamentales para crear aplicaciones web interactivas y escalables. Puedes extender este proyecto agregando más filtros, mejorando los estilos o integrando otras APIs. ¡Experimenta y adapta estas ideas a tus necesidades!