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.jsx
const 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.jsx
const 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.jsx
const 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.jsx
import { 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.jsx
export 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.jsx
export 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.jsx
export 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.jsx
function 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.jsx
export 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.jsx
function 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 API
document.querySelector('form').reset()
// Con ref
formRef.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.