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:

  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.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:

  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.