Miguel Angel Reyes Moreno
Jean Bustamante
Sí quedó mejor como lo hiciste :d
JOSE FABIAN BONILLA GUZMAN
definitivamente mucho mas sencillo por este lado,desde mi pequeña experiencia,
Construir un buscador funcional en React requiere conectar el input con el estado global y aplicar lógica de filtrado sobre los productos que vienen de una API. Aprenderás a filtrar productos por título usando filter, toLowerCase e includes, y a renderizar vistas condicionales según lo que escriba el usuario.
¿Cómo separar los items originales de los items filtrados en el estado global?
La primera decisión de arquitectura es no sobrescribir los productos originales que llegan de la API. Si pisas ese array, pierdes la fuente de datos.
La solución es duplicar el estado: mantienes items con todos los productos y creas un nuevo estado llamado filteredItems que guardará únicamente los resultados de la búsqueda [01:05]. Así el setter setFilteredItems actualiza solo la lista visible cuando el usuario escribe en el input, sin tocar la data original.
¿Por qué necesito dos estados separados para el buscador? Porque
itemsguarda todos los productos de la API yfilteredItemsguarda solo los que coinciden con la búsqueda. Si usas uno solo, pierdes la lista completa al filtrar.
¿Cómo filtrar productos por título con filter, toLowerCase e includes?
La función de filtrado recibe dos parámetros: el array de items y el texto del input, llamado searchByTitle [02:10]. Dentro, se usa el método filter para evaluar cada producto y devolver solo los que coincidan.
La clave está en normalizar ambos strings con toLowerCase antes de comparar. Esto garantiza que la búsqueda funcione sin importar si el usuario escribe en mayúsculas o minúsculas. Después, includes verifica si el título del producto contiene el texto buscado.
Los pasos concretos del filtrado son:
items y searchByTitle como parámetros de la función..filter() sobre el array de items.item.title a minúsculas con .toLowerCase().searchByTitle a minúsculas..includes() para comparar coincidencias parciales.Extraer esta lógica a una función llamada filteredItemsByTitle mantiene el código limpio y evita líneas demasiado largas dentro del useEffect.
¿Cómo renderizar la vista condicional según la búsqueda del usuario?
El componente Home originalmente pintaba todas las cards desde context.items. Ahora necesita decidir qué mostrar según tres escenarios distintos.
La solución es crear una constante llamada renderView que evalúa el estado del input y los resultados [06:30]. Si no hay búsqueda activa, muestra el catálogo completo. Si hay búsqueda con coincidencias, muestra los productos filtrados. Si hay búsqueda sin resultados, muestra un mensaje amigable.
¿Qué condiciones debe evaluar renderView?
La lógica condicional sigue este orden:
searchByTitle existe y su length es mayor a cero, evalúa los resultados filtrados.filteredItems.length es mayor a cero, renderiza esas cards.items.¿Qué pasa si el usuario busca algo que no existe? El componente debe retornar un mensaje claro como "no hay coincidencias" en lugar de dejar la pantalla en blanco. Una pantalla vacía rompe la experiencia del usuario.
¿Por qué exportar filteredItems desde el contexto es obligatorio?
Un error frecuente es crear el estado filteredItems dentro del Provider pero olvidar incluirlo en el value que se expone al resto de la app [10:15]. Sin esa exportación, el componente Home nunca recibe la data filtrada y la búsqueda parece rota aunque la lógica esté correcta. Revisa siempre que el nuevo estado quede listado junto a los demás valores del contexto.
¿Cómo conectar el useEffect con el filtrado dinámico?
El useEffect observa cambios en items y searchByTitle. Cuando el usuario escribe, se ejecuta la función de filtrado y el resultado se guarda con setFilteredItems [04:20].
Dentro del useEffect se valida que searchByTitle exista antes de ejecutar el filtro, porque no siempre habrá búsqueda activa. El usuario puede entrar a Home sin escribir nada, y en ese caso el catálogo completo debe mostrarse normalmente.
El flujo completo queda así:
searchByTitle en el contexto.filteredItemsByTitle.setFilteredItems guarda los resultados en el estado global.filteredItems desde el contexto y los renderiza.Probar el buscador con palabras como License o rustic confirma que el filtrado por título funciona correctamente, mientras que un texto sin coincidencias activa el mensaje de error controlado.
El reto: filtrar productos por categoría
Ahora viene tu turno. Aplica la misma lógica que usaste para filtrar por título, pero esta vez por categoría. La idea es que al hacer clic en clothes veas solo prendas, y al elegir electronics aparezcan únicamente productos electrónicos.
¿Te animas a resolverlo antes de la siguiente clase? Cuéntame en los comentarios cómo estructuraste tu solución.
Miguel Angel Reyes Moreno
Jean Bustamante
Sí quedó mejor como lo hiciste :d
JOSE FABIAN BONILLA GUZMAN
definitivamente mucho mas sencillo por este lado,desde mi pequeña experiencia,
Yo tengo esta solución, que me pareció mas simple .
const [searchByTitle, setSearchByTitle] = useState('');
useEffect(() => { if (searchByTitle.length > 0) { setFilteredProducts(filterProductsByTitle(products, searchByTitle)); } else { setFilteredProducts(products); } }, [products, searchByTitle]);
.map() del home:<div className='grid grid-cols-4 w-full max-w-screen-lg'> {filteredProducts.map((product) => ( <Card key={product.id} item={product} /> ))} <ProductDetail /> </div>
Diego Geovanny Bracero
Fernando Orozco Velasquez
lol
Jose Ever Muñoz Muñoz
te daran el sueldo minimo!
Carlos Restrepo
Creo que el arreglo original de items no debería incluirse en el estado. Yo debería tener una constante que almacene ese arreglo después de hacer un llamado a la función que hace el fetch, y hacerle un freeze() para volverlo inmutable. No debería ponerlo en el estado porque eso lo hace susceptible de ser modificado así sea por error. Ese arreglo nunca debe cambiar. El filtered en cambio si debe estar en el estado, se espera que cambie cada vez que cambia el contenido del input. Lo que esperamos que no cambie nunca no debe ponerse en el estado. Cuando el input esté vacío, se puede setear el filtered con un spread (copia) del arreglo de items original inmutable para que haga re-render. No es algo crítico, pero ayuda a evitar errores.
Mario Alejandro Cuastumal Vallejos
A mi me funciono creando un estado derivado de ítems, donde se filtran según la búsqueda que hagamos.
const filteredItems=items?.filter(todo=>todo.title.toLowerCase().includes(searchByTitle.toLowerCase()))
Luego lo envié y lo recibí en Home, para realizar el renderizado directamente.
{filteredItems?.map(item => ( <Card key={item.id} data={item} /> ))}
Es correcto?, o es mejor como lo realiza en la clase?
Alexandra Gabriela
me sirvio muy bien
Juan Mercado
Aqui si que me enrede un poquito :(
Fidel Parabacuto
Maia Paratcha
Buenas a todos! Yo lo hice un poco distinto. Acepto criticas constructivas: *Home/index.jsx: //Agregue property value en el imput:
<input type="text" placeholder='Search a product' className='rounded-lg border border-black p-4 mb-4 w-3/4 focus:outline-none' value={context.searchByTitle} onChange={(event) => context.setSearchByTitle(event.target.value) } /> <div className='grid gap-1 grid-cols-4 w-full max-w-screen-lg'> { context.filteredItemsByTitle?.map(item => ( <Card key={item.id} data={item} /> )) } </div>
En Context/index.jsx: //No agrego el estado filteredItems, solo hago una funcion filteredItemsByTitle y luego lo pase en el provider:
const filteredItemsByTitle = items?.filter( (item) => { const itemTitle = item.title.toLowerCase(); const searchTitle = searchByTitle.toLowerCase(); return itemTitle.includes(searchTitle); } );
Julio Bastidas
pregunta, no es mejor hacer la busqueda a la api?
Jhon Alexis Duque Padilla
Si, pero la clase nos explica como trabajar el filtro de manera local
Gianluca Enzo Procopio
Yo lo hice directamente con el render principal, ya que todos los productos en su titulo tienen un espacio:
{loading && products.map(product=>{ if(product.title.toLowerCase().includes(searchValue.toLowerCase())){ return<Card key={product.id} name={product.title} category={product.category.name} img={product.images[0]} price={product.price} description={product.description} id={product.id} /> } })}
Carlos Garcia
Por aquí dejo un hook useDebounce para que no se haga la búsqueda cada vez que se escriba.
import { useEffect, useState } from 'react'; export const useDebounce = (value: string, delay: number) => { const [debounceValue, setDebounceValue] = useState(''); useEffect(() => { const timeoutId = setTimeout(() => setDebounceValue(value), delay); return () => { clearTimeout(timeoutId); }; }, [value]); return debounceValue; };
Jhon Sebastián Delgado Almendrales
Siento que se complicó un poco con el filtrado. Pero como dicen todos, en la programación siempre hay manera diferentes de hacerlo.
Lo que hice fue crear una constante en el context, que me filtrara apartir del valor de "searchBytitle". Y esa constante, es la que retorno en el provider del context para que se utilizada en la home:
Jose Ever Muñoz Muñoz
muy buen codigo
Ulqernesh Karvenae
Reto Completado
Brenda Sutara
Nery Alberto Cano Ortigoza
A mi se me ocurrio poner un else que llene los items en filtered items para dejarlo a como funciona por default
useEffect(() => { if (searchByTitle) { setFilteredItems(filteredItemsByTitle(items, searchByTitle)); } else { setFilteredItems(items); } }, [items, searchByTitle]);
Juan Diaz
El API de Platzi esta algo Creepy!
Jean Bustamante
Si le ponen un <div></div> y un <div></div> después del elemento de "We don't have anything to show" el texto se centra. Sólo así conseguí centrarlo, aunque no en todos los tamaños.
Diego Eduardo Téllez Contreras
Lo que pasa es que como estamos en un grid que tiene 4 columnas pues el texto está quedando en la primera columna solamente. Yo lo que hice fue decirle que mi div va a ocupar las 4 columnas y usé flex para centrarlo:
Queda así:
Juan David Santamaria Gomez
hola buen dia, porque en ocasiones las API no retorna las imagenes de los productos???
Andres Felipe Bravo Hernandez
Es una API publica algun chistoso cambia el formato de las imagenes, si revisas la pestana de red al hacer la peticion podras ver que si retorna las imagenes pero al parecer en algun lado le hacen un Stringify y no retorna el arreglo de imagenes si no un string de el arreglo.
Ricardo Collado
Hola, esto es una pregunta.
Una persona con mucho mas experiencia, me ha dicho que es mala idea, colocar los "useEffect" o manejos directos al dom, en un "custom hooks", que alli, solo deberiamos tener el fetch a data, y de hecho, me han indicado que se debe evitar a toda costa el uso de los useEffect.
Que tan cierto es eso?
Miguel Sosa
Yo renderizaría siempre filteredItems y el primer estado de esta variable sería igual a items ya que no hay ninguna búsqueda. El resultado es el mismo y reduce una cantidad de código.
Simplifiquemos el código:
const renderView = () => { const itemsToRender = context.SearchByTitle?.length > 0 ? context.FilteredItems : context.Items; if (itemsToRender?.length > 0) { return itemsToRender.map(item => ( <Card key={item.id} data={item} /> )); } else { return <p>No Results Found</p>; } };
Es costoso, a nivel de rendimiento de la app, filtrar por cada tecla que presiona un user.
Por eso creé un customHook para esperar a que el cliente termine de teclear la palabra que desea buscar y una vez transcurrido un tiempo específico se ejecute el filtrado
import { useState, useEffect } from 'react'; export const useDebounce = (value: string, delay: number) => { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(handler); }; }, [value, delay]); return debouncedValue; }; ```Se ejecuta en mi context:  ```js const debouncedSearch = useDebounce(search, 500); useEffect(() => { if (debouncedSearch.length > 0) { setFilteredItems(items.filter(item => item.title.toLowerCase().includes(debouncedSearch.toLowerCase()))); } else { setFilteredItems(items); } }, [debouncedSearch, items])
Yo me llevé el div a renderView asi se me veia todo completo y las columnas aparecen cuando sea necesario. Me quedaba el "no hay resultados" en una sola columna de las 4
const renderView = () => { if (context.searchValue?.length > 0) { if (context.filteredItems?.length > 0) { return ( <div className='flex pb-10 flex-col md:grid md:grid-cols-3 lg:grid lg:grid-cols-4 mx-auto items-center w-4/5 justify-center gap-4 md:gap-6 lg:gap-10'> {context.filteredItems?.map((item) => { return <Card key={item.id} {...item} />; })} </div> ) } else { return ( <div className='flex flex-col justify-center items-center space-x-2 mx-auto py-28'> <img className='w-28 ' src="imagen" alt="no results" /> <h2 className='text-center text-3xl opacity-60 py-6'>No Result Found</h3> <p className='text-center text-xl opacity-60'>We can't find any item matching your search</p> </div> ) } } else { return ( <div className='flex pb-10 flex-col md:grid md:grid-cols-3 lg:grid lg:grid-cols-4 mx-auto items-center w-4/5 justify-center gap-4 md:gap-6 lg:gap-10'> {context.items?.map((item) => { return <Card key={item.id} {...item} />; })} </div> ) } }