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:

  1. Implementar el resto de filtros - Añadir filtros adicionales que aún no hemos implementado
  2. Usar onFocus y onBlur - Mejorar la UX con feedback visual al interactuar con los inputs
  3. 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 name con useId()
  • Captura el valor en handleSubmit con FormData
  • 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 filters en App.jsx incluye todos los campos
  • La función jobsFilteredByFilters aplica 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 onFocus y onBlur
  • 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ísticaTodo onChangeTodo onSubmitHí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.