Saltar al contenido principal
Logo de JSCamp

Gestión de Formularios y Filtros

Aprende a manejar formularios con FormData, implementar filtros dinámicos y levantar estado al componente padre

midudev

Más clases y directos en YouTube

Suscribirse

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:

  1. Creamos un useId() para cada campo del formulario
  2. Usamos esos IDs como valores para name e id
  3. En el handleSubmit, usamos FormData para 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:

  1. App tiene los datos de trabajos (jobsData)
  2. App necesita filtrar los trabajos antes de paginarlos
  3. 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 submithandleSubmit captura valores con FormDataonSearch(filters) llama a la función del padreApp 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, experienceLeveljobsFilteredByFilters2. Calcular páginas totales3. Aplicar paginaciónpagedResults (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 inputonChange dispara handleChangeTextonChangeText(value) llama a la función del padreApp actualiza textToFilter con setTextToFilterjobsWithTextFilter se recalcula automáticamenteLos 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:

  1. Si textToFilter está vacío → no filtrar, devolver todos
  2. Si textToFilter tiene 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:

  1. Recalcula jobsFilteredByFilters
  2. Recalcula jobsWithTextFilter
  3. Recalcula totalPages
  4. Recalcula pagedResults
  5. Re-renderiza JobListings y Pagination

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.