Aprende a manejar formularios con FormData, implementar filtros dinámicos y levantar estado al componente padre
midudev
Más clases y directos en YouTube
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.jsximport { 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.jsxfunction 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.jsxexport 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.jsxfunction 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.jsxexport 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.jsxfunction 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.
