Gestión de Formularios y Filtros
En la clase anterior añadimos el handleSubmit y usamos useId() para generar identificadores únicos. Ahora vamos a implementar la lógica completa del formulario: capturar los valores, filtrar los trabajos y añadir búsqueda en tiempo real.
Capturando los datos del formulario con FormData
Vamos a mejorar nuestro handleSubmit para capturar los valores del formulario:
// src/components/SearchForm.jsx
import { useId } from 'react'
export function SearchForm() {
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),
}
console.log(filters)
}
return (
<form className="search-form" onSubmit={handleSubmit}>
{/* ... */}
</form>
)
}
Puntos clave:
- Creamos un
useId()para cada campo del formulario - Usamos esos IDs como valores para
nameeid - En el
handleSubmit, usamosFormDatapara obtener los valores
¿Qué es FormData?
FormData es una API nativa del navegador que permite acceder fácilmente a los datos de un formulario.
Creación de FormData
const formData = new FormData(event.target)
// ↑
// El elemento <form>
event.target es el elemento del formulario que disparó el evento submit.
Obtener valores con .get()
const formData = new FormData(event.target)
// Obtener el valor de un campo por su atributo "name"
const value = formData.get('nombre-del-campo')
En nuestro caso:
const filters = {
search: formData.get(idText), // Valor del input de texto
technology: formData.get(idTechnology), // Valor del select de tecnología
location: formData.get(idLocation), // Valor del select de ubicación
experienceLevel: formData.get(idExperienceLevel), // Valor del select de experiencia
}
Como usamos useId() para los atributos name, pasamos esos IDs a .get().
Ejemplo visual
HTML generado:
<input type="text" name=":r0:" id=":r0:" />
<select name=":r1:" id=":r1:">
<option value="react">React</option>
</select>
JavaScript:
const formData = new FormData(event.target)
formData.get(':r0:') // Valor del input (lo que el usuario escribió)
formData.get(':r1:') // "react" (opción seleccionada)
Ventajas de FormData
- ✅ Simple - Una línea de código para acceder al formulario
- ✅ Nativo - No necesitas librerías externas
- ✅ Funciona con cualquier input - text, select, checkbox, radio, file, etc.
- ✅ No necesitas estado - Los valores están en el DOM
Levantando el estado al padre
Los filtros deben estar en el componente App porque:
Apptiene los datos de trabajos (jobsData)Appnecesita filtrar los trabajos antes de paginarlos- Múltiples componentes pueden necesitar acceder a los filtros
Paso 1: Añadir estado en App.jsx
// src/App.jsx
function App() {
const [filters, setFilters] = useState({
technology: '',
location: '',
experienceLevel: '',
})
const [currentPage, setCurrentPage] = useState(1)
// Por ahora solo log
const handleSearch = (newFilters) => {
console.log('Filtros recibidos:', newFilters)
}
/* ... resto del código */
}
export default App
Estados añadidos:
filters- Filtros de los selects (technology, location, experienceLevel)textToFilter- Texto del input de búsqueda (filtro en tiempo real)currentPage- Página actual (ya lo teníamos)
Paso 2: Pasar onSearch a SearchForm
// src/components/SearchForm.jsx
export function SearchForm({ onSearch }) {
// ... useId's
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),
}
// Llamar a la función del padre
onSearch(filters)
}
// ... return
}
Flujo de datos:
Usuario hace submit
↓
handleSubmit captura valores con FormData
↓
onSearch(filters) llama a la función del padre
↓
App recibe los filtros y actualiza el estado
Filtrando los trabajos
Ahora vamos a filtrar los trabajos antes de paginarlos:
// src/App.jsx
function App() {
const [filters, setFilters] = useState({
technology: '',
location: '',
experienceLevel: '',
})
const [currentPage, setCurrentPage] = useState(1)
const handleSearch = (newFilters) => {
setFilters({
technology: newFilters.technology,
location: newFilters.location,
experienceLevel: newFilters.experienceLevel,
})
setTextToFilter(newFilters.search)
}
const handlePageChange = (page) => {
setCurrentPage(page)
}
// 1. Filtrar por los selects
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)
)
})
// 2. Paginar los resultados ya filtrados
const totalPages = Math.ceil(jobsFilteredByFilters.length / RESULTS_PER_PAGE)
const pagedResults = jobsFilteredByFilters.slice(
(currentPage - 1) * RESULTS_PER_PAGE,
currentPage * RESULTS_PER_PAGE
)
return (
<>
<Header />
<main>
<SearchForm onSearch={handleSearch} />
<JobListings jobs={pagedResults} />
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
/>
</main>
<Footer />
</>
)
}
Orden de operaciones:
jobsData (todos los trabajos)
↓
1. Filtrar por technology, location, experienceLevel
↓
jobsFilteredByFilters
↓
↓
2. Calcular páginas totales
↓
3. Aplicar paginación
↓
pagedResults (solo los trabajos de la página actual)
Entendiendo el filtro por selects
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)
)
})
Lógica de cada condición:
filters.technology === '' || job.data.technology === filters.technology
// ↑ ↑
// Si está vacío O Si coincide con el filtro
// (no filtrar) (sí filtrar)
Ejemplos:
// Filtro: { technology: '', location: 'remoto', experienceLevel: '' }
// Job 1: { technology: 'react', modalidad: 'remoto', nivel: 'senior' }
true && true && true = ✅ Pasa el filtro (location coincide)
// Job 2: { technology: 'python', modalidad: 'cdmx', nivel: 'junior' }
true && false && true = ❌ No pasa (location no coincide)
// Job 3: { technology: 'node', modalidad: 'remoto', nivel: 'mid' }
true && true && true = ✅ Pasa el filtro (location coincide)
Filtro de texto en tiempo real
Ahora vamos a añadir un filtro que se ejecute cada vez que el usuario escribe en el input de búsqueda:
Paso 1: Añadir onChange al input
// src/components/SearchForm.jsx
export function SearchForm({ onSearch, onChangeText }) {
/* ... */
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}
/>
</div>
{/* ... resto de los campos */}
<button type="submit">Buscar</button>
</form>
)
}
Paso 2: Manejar el cambio en App.jsx
// src/App.jsx
function App() {
const [filters, setFilters] = useState({
technology: '',
location: '',
experienceLevel: '',
})
const [textToFilter, setTextToFilter] = useState('') // <-----
const [currentPage, setCurrentPage] = useState(1)
// ... resto del código
}
Flujo del filtro en tiempo real:
Usuario escribe en el input
↓
onChange dispara handleChangeText
↓
onChangeText(value) llama a la función del padre
↓
App actualiza textToFilter con setTextToFilter
↓
jobsWithTextFilter se recalcula automáticamente
↓
Los trabajos filtrados se muestran inmediatamente
Entendiendo el filtro por texto
Ahora en App.jsx vamos a filtrar los trabajos por texto:
const jobsWithTextFilter =
textToFilter === ''
? jobsFilteredByFilters
: jobsFilteredByFilters.filter((job) => {
return job.titulo.toLowerCase().includes(textToFilter.toLowerCase())
})
Lógica:
- Si
textToFilterestá vacío → no filtrar, devolver todos - Si
textToFiltertiene texto → filtrar por título
Ejemplo:
// textToFilter = "desarrollador"
// Job 1: { titulo: "Desarrollador Frontend" }
"desarrollador frontend".includes("desarrollador") = ✅ true
// Job 2: { titulo: "Analista de Datos" }
"analista de datos".includes("desarrollador") = ❌ false
// Job 3: { titulo: "Desarrollador Senior React" }
"desarrollador senior react".includes("desarrollador") = ✅ true
Usamos .toLowerCase() para hacer la búsqueda insensible a mayúsculas:
'Desarrollador'.toLowerCase() // "desarrollador"
'DESARROLLADOR'.toLowerCase() // "desarrollador"
'dEsArRoLlAdOr'.toLowerCase() // "desarrollador"
Orden completo de estados y filtrado
Aquí está el flujo completo de cómo funcionan los estados:
function App() {
// 1. Estados
const [filters, setFilters] = useState({
technology: '',
location: '',
experienceLevel: '',
})
const [textToFilter, setTextToFilter] = useState('')
const [currentPage, setCurrentPage] = useState(1)
// 2. Handlers
const handleSearch = (newFilters) => {
setFilters({
technology: newFilters.technology,
location: newFilters.location,
experienceLevel: newFilters.experienceLevel,
})
setTextToFilter(newFilters.search)
}
const handleChangeText = (text) => {
setTextToFilter(text)
}
const handlePageChange = (page) => {
setCurrentPage(page)
}
// 3. Filtrado en cascada
// Paso 1: Filtrar por selects
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)
)
})
// Paso 2: Filtrar por texto
const jobsWithTextFilter =
textToFilter === ''
? jobsFilteredByFilters
: jobsFilteredByFilters.filter((job) => {
return job.titulo.toLowerCase().includes(textToFilter.toLowerCase())
})
// 4. Paginación
const totalPages = Math.ceil(jobsWithTextFilter.length / RESULTS_PER_PAGE)
const pagedResults = jobsWithTextFilter.slice(
(currentPage - 1) * RESULTS_PER_PAGE,
currentPage * RESULTS_PER_PAGE
)
// 5. Render
return (
<>
<Header />
<main>
<SearchForm onSearch={handleSearch} onChangeText={handleChangeText} />
<JobListings jobs={pagedResults} />
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
/>
</main>
<Footer />
</>
)
}
Diagrama del flujo de datos
╔════════════════════════════════════╗
║ Estados en App ║
║ - filters (technology, location) ║
║ - textToFilter ║
║ - currentPage ║
╚════════════════════════════════════╝
↓
╔════════════════════════════════════╗
║ jobsData (todos) ║
╚════════════════════════════════════╝
↓
Filtro 1: Selects
↓
╔════════════════════════════════════╗
║ jobsFilteredByFilters ║
╚════════════════════════════════════╝
↓
Filtro 2: Texto
↓
╔════════════════════════════════════╗
║ jobsWithTextFilter ║
╚════════════════════════════════════╝
↓
Calcular totalPages
↓
Aplicar slice()
↓
╔════════════════════════════════════╗
║ pagedResults ║
║ (5 trabajos máximo) ║
╚════════════════════════════════════╝
↓
┌─────────────────┐
│ JobListings │
│ Renderiza los │
│ 5 trabajos │
└─────────────────┘
Diferencia entre submit y onChange
Submit (botón “Buscar”)
<form onSubmit={handleSubmit}>
<select name={idTechnology}>{/* ... */}</select>
<select name={idLocation}>{/* ... */}</select>
<button type="submit">Buscar</button>
</form>
- Se ejecuta al hacer clic en “Buscar”
- Se ejecuta al presionar Enter en cualquier input
- Actualiza todos los filtros a la vez
- Útil para filtros que no necesitan actualizarse constantemente
onChange (tiempo real)
<input type="text" onChange={handleChangeText} />
- Se ejecuta cada vez que cambia el valor
- Actualiza en tiempo real mientras escribes
- Útil para búsquedas de texto donde quieres ver resultados inmediatamente
Ventajas de esta arquitectura
1. ✅ Estado centralizado
App (tiene todos los estados)
↓
├─→ SearchForm (recibe onSearch y onChangeText)
├─→ JobListings (recibe trabajos ya filtrados)
└─→ Pagination (recibe totalPages calculadas)
- El estado está en un solo lugar
- Fácil de debuggear
- Los componentes hijos son más simples
2. ✅ Componentes reutilizables
// SearchForm solo se preocupa de capturar datos
<SearchForm onSearch={handleSearch} onChangeText={handleChangeText} />
// JobListings solo se preocupa de renderizar
<JobListings jobs={pagedResults} />
// Pagination solo se preocupa de la navegación
<Pagination currentPage={currentPage} totalPages={totalPages} />
3. ✅ Filtrado en cascada
Datos → Filtro 1 → Filtro 2 → Paginación → Render
- Cada paso procesa el resultado del anterior
- Fácil de añadir más filtros
- Fácil de cambiar el orden
4. ✅ Reactividad automática
Cuando cambias cualquier estado, React automáticamente:
- Recalcula
jobsFilteredByFilters - Recalcula
jobsWithTextFilter - Recalcula
totalPages - Recalcula
pagedResults - Re-renderiza
JobListingsyPagination
No necesitas hacer nada más, React lo hace por ti.
Lo que hemos visto en esta clase
- 📝 FormData - API nativa para acceder a datos de formularios
- 🔄 Levantar estado - Mover el estado al componente padre
- 🎯 Props de callback - Pasar funciones a componentes hijos
- 🔍 Filtrado en cascada - Aplicar múltiples filtros secuencialmente
- ⚡ Filtro en tiempo real - Con onChange para búsqueda instantánea
- 📊 Orden de operaciones - Filtrar primero, paginar después
- 🏗️ Arquitectura limpia - Estado centralizado, componentes reutilizables
- 🎨 Flujo de datos - De padre a hijo con props, de hijo a padre con callbacks
En la próxima clase exploraremos más conceptos avanzados de React como custom hooks y optimización de rendimiento.
💡 Recuerda: El orden importa: primero filtras los datos, luego calculas las páginas totales, y finalmente aplicas la paginación. El estado debe estar en el componente padre que necesita manipular los datos, no en los componentes hijos que solo los muestran.