Practica implementando filtros, eventos onFocus/onBlur y gestión de formularios en tiempo real
midudev
Más clases y directos en YouTube
Ejercicios: Formularios y Eventos
En esta clase vamos a practicar todo lo que hemos aprendido sobre gestión de formularios, filtros y eventos en React. Estos ejercicios te ayudarán a consolidar los conceptos de la clase anterior y a mejorar tu aplicación DevJobs.
Objetivo de los ejercicios
Los ejercicios de esta clase se centran en mejorar la experiencia de usuario del formulario de búsqueda:
- Implementar el resto de filtros - Añadir filtros adicionales que aún no hemos implementado
- Usar onFocus y onBlur - Mejorar la UX con feedback visual al interactuar con los inputs
- Pasar eventos a tiempo real o usar submit - Decidir qué filtros se aplican instantáneamente y cuáles al hacer submit
Ejercicio 1: Implementar el resto de filtros
En la clase anterior implementamos filtros básicos, pero aún nos faltan algunos. Vamos a completar todos los filtros disponibles en el formulario.
Filtros a implementar
Si tu formulario tiene estos campos pero no están funcionando todavía, es hora de implementarlos:
1. Filtro por tecnología
// src/App.jsxconst jobsFilteredByFilters = jobsData.filter((job) => { return ( (filters.technology === '' || job.data.technology === filters.technology) && (filters.location === '' || job.data.modalidad === filters.location) && (filters.experienceLevel === '' || job.data.nivel === filters.experienceLevel) )})
Si aún no lo tienes implementado:
- Añade un select con las tecnologías disponibles (React, Node, Python, etc.)
- Asegúrate de que el campo tenga un
nameconuseId() - Captura el valor en
handleSubmitconFormData - Añade la tecnología al estado
filters
2. Filtro por salario
// src/components/SearchForm.jsxconst idSalary = useId()return ( <form className="search-form" onSubmit={handleSubmit}> {/* ... otros campos ... */} <div className="form-group"> <label htmlFor={idSalary}>Salario mínimo</label> <input type="number" name={idSalary} id={idSalary} placeholder="30000" min="0" step="1000" /> </div> <button type="submit">Buscar</button> </form>)
En App.jsx, añade el filtro de salario:
const jobsFilteredByFilters = jobsData.filter((job) => { // Convertir el salario del job a número (asumiendo que job.data.salary es un string) const jobSalary = parseInt(job.data.salary) || 0 const minSalary = filters.salary ? parseInt(filters.salary) : 0 return ( (filters.technology === '' || job.data.technology === filters.technology) && (filters.location === '' || job.data.modalidad === filters.location) && (filters.experienceLevel === '' || job.data.nivel === filters.experienceLevel) && jobSalary >= minSalary )})
3. Filtro por tipo de contrato
// src/components/SearchForm.jsxconst idContractType = useId()<div className="form-group"> <label htmlFor={idContractType}>Tipo de contrato</label> <select name={idContractType} id={idContractType}> <option value="">Todos</option> <option value="full-time">Full Time</option> <option value="part-time">Part Time</option> <option value="freelance">Freelance</option> <option value="internship">Prácticas</option> </select></div>
Añade el filtro en App.jsx:
const jobsFilteredByFilters = jobsData.filter((job) => { return ( // ... filtros anteriores ... filters.contractType === '' || job.data.contractType === filters.contractType )})
Checklist del Ejercicio 1
- Todos los selects del formulario tienen su filtro correspondiente
- Los filtros se capturan correctamente con
FormData - El estado
filtersenApp.jsxincluye todos los campos - La función
jobsFilteredByFiltersaplica todos los filtros correctamente - Los resultados se filtran correctamente cuando cambias cada select
Ejercicio 2: Usar onFocus y onBlur
Los eventos onFocus y onBlur mejoran la experiencia de usuario dando feedback visual cuando el usuario interactúa con los campos del formulario.
¿Qué son onFocus y onBlur?
<input onFocus={() => console.log('El input está activo')} onBlur={() => console.log('El input perdió el foco')}/>
- onFocus - Se ejecuta cuando el usuario hace clic en el input (o usa Tab para llegar a él)
- onBlur - Se ejecuta cuando el usuario sale del input (hace clic fuera o presiona Tab)
Implementación con estado local
Vamos a añadir un borde de color cuando un campo está activo:
// src/components/SearchForm.jsximport { useId, useState } from 'react'export function SearchForm({ onSearch, onChangeText }) { const idText = useId() const idTechnology = useId() const idLocation = useId() const idExperienceLevel = useId() // Estado para saber qué campo está activo const [focusedField, setFocusedField] = useState(null) const handleSubmit = (event) => { event.preventDefault() const formData = new FormData(event.target) const filters = { search: formData.get(idText), technology: formData.get(idTechnology), location: formData.get(idLocation), experienceLevel: formData.get(idExperienceLevel), } onSearch(filters) } const handleChangeText = (event) => { onChangeText(event.target.value) } return ( <form className="search-form" onSubmit={handleSubmit}> <div className="form-group"> <label htmlFor={idText}>Búsqueda</label> <input type="text" name={idText} id={idText} placeholder="Buscar trabajos..." onChange={handleChangeText} onFocus={() => setFocusedField('search')} onBlur={() => setFocusedField(null)} style={{ borderColor: focusedField === 'search' ? '#4f46e5' : '#d1d5db', outline: focusedField === 'search' ? '2px solid #4f46e5' : 'none', }} /> {focusedField === 'search' && ( <small className="input-hint">Busca por título de trabajo, empresa o tecnología</small> )} </div> <div className="form-group"> <label htmlFor={idTechnology}>Tecnología</label> <select name={idTechnology} id={idTechnology} onFocus={() => setFocusedField('technology')} onBlur={() => setFocusedField(null)} style={{ borderColor: focusedField === 'technology' ? '#4f46e5' : '#d1d5db', }} > <option value="">Todas</option> <option value="react">React</option> <option value="node">Node.js</option> <option value="python">Python</option> </select> </div> {/* ... resto de los campos con onFocus y onBlur ... */} <button type="submit">Buscar</button> </form> )}
¿Cómo funciona?
Flujo del evento:
Usuario hace clic en el input ↓onFocus se ejecuta ↓setFocusedField('search') ↓El componente se re-renderiza ↓El input muestra el borde de color ↓Usuario hace clic fuera del input ↓onBlur se ejecuta ↓setFocusedField(null) ↓El componente se re-renderiza ↓El input vuelve a su estado normal
Mejorando con CSS en lugar de inline styles
En lugar de usar style inline, puedes usar clases CSS:
// src/components/SearchForm.jsx<input type="text" name={idText} id={idText} placeholder="Buscar trabajos..." onChange={handleChangeText} onFocus={() => setFocusedField('search')} onBlur={() => setFocusedField(null)} className={focusedField === 'search' ? 'input-focused' : ''}/>
Y en tu CSS:
/* src/styles/SearchForm.css */.form-group input,.form-group select { border: 2px solid #d1d5db; transition: border-color 0.2s ease;}.input-focused { border-color: #4f46e5 !important; outline: 2px solid #4f46e5;}.input-hint { display: block; margin-top: 0.25rem; font-size: 0.875rem; color: #6b7280;}
Checklist del Ejercicio 2
- Los inputs tienen eventos
onFocusyonBlur - Hay estado local para saber qué campo está activo
- Los campos activos muestran feedback visual (borde de color, outline, etc.)
- Opcionalmente: Muestra hints o mensajes de ayuda cuando un campo está activo
- El feedback visual se elimina cuando el usuario sale del campo
Ejercicio 3: Pasar eventos a tiempo real o usar submit
En este ejercicio vas a decidir qué filtros se aplican en tiempo real y cuáles solo al hacer submit del formulario.
Estrategias de filtrado
Opción A: Todo en tiempo real (onChange)
// src/components/SearchForm.jsxexport function SearchForm({ onSearch }) { const idText = useId() const idTechnology = useId() const idLocation = useId() const handleChange = (event) => { const formData = new FormData(event.target.form) const filters = { search: formData.get(idText), technology: formData.get(idTechnology), location: formData.get(idLocation), } onSearch(filters) } return ( <form className="search-form"> <input type="text" name={idText} id={idText} onChange={handleChange} /> <select name={idTechnology} id={idTechnology} onChange={handleChange}> <option value="">Todas</option> <option value="react">React</option> </select> <select name={idLocation} id={idLocation} onChange={handleChange}> <option value="">Todas</option> <option value="remoto">Remoto</option> </select> {/* No necesitas botón de submit */} </form> )}
Ventajas:
- ✅ Resultados instantáneos
- ✅ UX más fluida
- ✅ No necesitas botón de submit
Desventajas:
- ❌ Muchas re-renderizaciones
- ❌ Puede ser lento con muchos datos
- ❌ El usuario no puede “preparar” varios filtros antes de buscar
Opción B: Solo submit (onSubmit)
// src/components/SearchForm.jsxexport function SearchForm({ onSearch }) { const idText = useId() const idTechnology = useId() const idLocation = useId() const handleSubmit = (event) => { event.preventDefault() const formData = new FormData(event.target) const filters = { search: formData.get(idText), technology: formData.get(idTechnology), location: formData.get(idLocation), } onSearch(filters) } return ( <form className="search-form" onSubmit={handleSubmit}> <input type="text" name={idText} id={idText} /> <select name={idTechnology} id={idTechnology}> <option value="">Todas</option> <option value="react">React</option> </select> <select name={idLocation} id={idLocation}> <option value="">Todas</option> <option value="remoto">Remoto</option> </select> <button type="submit">Buscar</button> </form> )}
Ventajas:
- ✅ Mejor rendimiento
- ✅ El usuario puede preparar varios filtros
- ✅ Control sobre cuándo se ejecuta la búsqueda
Desventajas:
- ❌ Menos inmediato
- ❌ Requiere que el usuario haga clic en el botón
Opción C: Híbrido (Recomendado) ⭐
Búsqueda de texto en tiempo real + Selects con submit:
// src/components/SearchForm.jsxexport function SearchForm({ onSearch, onChangeText }) { const idText = useId() const idTechnology = useId() const idLocation = useId() const idExperienceLevel = useId() const handleSubmit = (event) => { event.preventDefault() const formData = new FormData(event.target) const filters = { search: formData.get(idText), technology: formData.get(idTechnology), location: formData.get(idLocation), experienceLevel: formData.get(idExperienceLevel), } onSearch(filters) } // Búsqueda de texto en tiempo real const handleChangeText = (event) => { onChangeText(event.target.value) } return ( <form className="search-form" onSubmit={handleSubmit}> {/* Input de texto: tiempo real */} <div className="form-group"> <label htmlFor={idText}>Búsqueda</label> <input type="text" name={idText} id={idText} placeholder="Buscar trabajos..." onChange={handleChangeText} /> </div> {/* Selects: solo con submit */} <div className="form-group"> <label htmlFor={idTechnology}>Tecnología</label> <select name={idTechnology} id={idTechnology}> <option value="">Todas</option> <option value="react">React</option> <option value="node">Node.js</option> </select> </div> <div className="form-group"> <label htmlFor={idLocation}>Ubicación</label> <select name={idLocation} id={idLocation}> <option value="">Todas</option> <option value="remoto">Remoto</option> <option value="cdmx">Ciudad de México</option> </select> </div> <div className="form-group"> <label htmlFor={idExperienceLevel}>Nivel de experiencia</label> <select name={idExperienceLevel} id={idExperienceLevel}> <option value="">Todos</option> <option value="junior">Junior</option> <option value="mid">Mid-level</option> <option value="senior">Senior</option> </select> </div> <button type="submit">Aplicar filtros</button> </form> )}
¿Por qué es mejor el enfoque híbrido?
- ✅ Input de texto en tiempo real - Para búsquedas rápidas, el usuario ve resultados mientras escribe
- ✅ Selects con submit - Para filtros múltiples, el usuario puede ajustar varios antes de buscar
- ✅ Mejor rendimiento - No re-renderizas todo cada vez que cambias un select
- ✅ Mejor UX - Combina inmediatez con control
Implementación del enfoque híbrido en App.jsx
// src/App.jsxfunction App() { const [filters, setFilters] = useState({ technology: '', location: '', experienceLevel: '', }) const [textToFilter, setTextToFilter] = useState('') const [currentPage, setCurrentPage] = useState(1) // Maneja el submit del formulario (selects) const handleSearch = (newFilters) => { setFilters({ technology: newFilters.technology, location: newFilters.location, experienceLevel: newFilters.experienceLevel, }) // También actualizamos el texto por si acaso setTextToFilter(newFilters.search || '') // Reiniciamos a la página 1 cuando cambian los filtros setCurrentPage(1) } // Maneja el cambio de texto en tiempo real const handleChangeText = (text) => { setTextToFilter(text) // Reiniciamos a la página 1 cuando cambia el texto setCurrentPage(1) } // ... resto del código de filtrado y paginación return ( <> <Header /> <main> <SearchForm onSearch={handleSearch} onChangeText={handleChangeText} /> <JobListings jobs={pagedResults} /> <Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={handlePageChange} /> </main> <Footer /> </> )}
Extra: Resetear la página al cambiar filtros
Cuando el usuario cambia los filtros, es buena práctica volver a la página 1:
const handleSearch = (newFilters) => { setFilters({ technology: newFilters.technology, location: newFilters.location, experienceLevel: newFilters.experienceLevel, }) setTextToFilter(newFilters.search || '') setCurrentPage(1) // ← Volver a la primera página}const handleChangeText = (text) => { setTextToFilter(text) setCurrentPage(1) // ← Volver a la primera página}
¿Por qué?
Imagina que estás en la página 5 mostrando trabajos de React. Si cambias el filtro a Python, puede que Python solo tenga 2 páginas de resultados. Sin el reset, seguirías en la página 5 que no existe para Python.
Checklist del Ejercicio 3
- Has decidido qué filtros van en tiempo real y cuáles con submit
- El input de búsqueda de texto funciona en tiempo real (onChange)
- Los selects se aplican al hacer submit del formulario
- Cuando cambias filtros, la página se resetea a 1
- El botón de submit dice algo descriptivo como “Aplicar filtros” o “Buscar”
- La UX es fluida y los filtros funcionan como esperas
Desafío extra: Botón de limpiar filtros
Añade un botón para limpiar todos los filtros y volver al estado inicial:
// src/components/SearchForm.jsxexport function SearchForm({ onSearch, onChangeText, onReset }) { // ... useId's y handlers const handleReset = () => { // Resetear el formulario document.querySelector('.search-form').reset() // Notificar al padre onReset() } return ( <form className="search-form" onSubmit={handleSubmit}> {/* ... campos del formulario ... */} <div className="form-actions"> <button type="submit" className="btn-primary"> Aplicar filtros </button> <button type="button" className="btn-secondary" onClick={handleReset}> Limpiar filtros </button> </div> </form> )}
En App.jsx:
const handleReset = () => { setFilters({ technology: '', location: '', experienceLevel: '', }) setTextToFilter('') setCurrentPage(1)}return <SearchForm onSearch={handleSearch} onChangeText={handleChangeText} onReset={handleReset} />
Desafío extra 2: Contador de resultados
Muestra cuántos trabajos se encontraron con los filtros actuales:
// src/App.jsxfunction App() { // ... estados y handlers const jobsFilteredByFilters = jobsData.filter((job) => { /* ... */ }) const jobsWithTextFilter = textToFilter === '' ? jobsFilteredByFilters : jobsFilteredByFilters.filter((job) => { /* ... */ }) const totalResults = jobsWithTextFilter.length return ( <> <Header /> <main> <SearchForm onSearch={handleSearch} onChangeText={handleChangeText} /> {/* Contador de resultados */} <div className="results-summary"> <p> Se encontraron <strong>{totalResults}</strong> trabajos {textToFilter && ` para "${textToFilter}"`} </p> </div> <JobListings jobs={pagedResults} /> <Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={handlePageChange} /> </main> <Footer /> </> )}
Lo que has practicado en estos ejercicios
- 🎯 Implementar filtros completos - Todos los campos del formulario filtran correctamente
- 👁️ Eventos onFocus y onBlur - Feedback visual al interactuar con inputs
- ⚡ Estrategias de filtrado - Tiempo real vs submit, y enfoque híbrido
- 🔄 Reset de página - Volver a página 1 al cambiar filtros
- 🧹 Limpiar filtros - Botón para resetear todos los filtros
- 📊 Contador de resultados - Mostrar cuántos trabajos se encontraron
- 🎨 UX mejorada - Hints, feedback visual y mejor usabilidad
Comparación de enfoques
| Característica | Todo onChange | Todo onSubmit | Híbrido ⭐ |
|---|---|---|---|
| Resultados instantáneos | ✅ | ❌ | ✅ (solo texto) |
| Rendimiento | ❌ | ✅ | ✅ |
| Control del usuario | ❌ | ✅ | ✅ |
| Filtros múltiples | ❌ | ✅ | ✅ |
| Mejor para búsqueda | ✅ | ❌ | ✅ |
| Mejor para filtros | ❌ | ✅ | ✅ |
Recomendación: Usa el enfoque híbrido para la mejor experiencia de usuario.
Recap de conceptos clave
FormData
const formData = new FormData(event.target)const value = formData.get('field-name')
onFocus y onBlur
<input onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} />
onChange para tiempo real
<input onChange={(e) => handleChange(e.target.value)} />
onSubmit para formularios
<form onSubmit={(e) => { e.preventDefault() handleSubmit()}}>
Reset de formulario
// Con DOM APIdocument.querySelector('form').reset()// Con refformRef.current.reset()
Siguientes pasos
Ahora que dominas la gestión de formularios en React, en las próximas clases aprenderemos:
- 🎣 Custom Hooks - Crear hooks reutilizables para lógica de formularios
- 📝 Validación de formularios - Validar datos antes de enviarlos
- 🚀 Optimización de rendimiento - useMemo y useCallback para formularios
💡 Recuerda: La experiencia de usuario es clave. Un formulario con búsqueda en tiempo real para texto y submit para filtros múltiples ofrece la mejor combinación de rapidez y control.
