Filtros automáticos y Routing básico

En esta clase vamos a mejorar significativamente la experiencia de usuario de nuestra aplicación eliminando el botón de búsqueda para que los filtros funcionen automáticamente. Además, aprenderemos a crear un sistema básico de páginas sin usar React Router.

Ajustes iniciales del formulario

Antes de empezar con los cambios, vamos a revisar algunos problemas visuales y funcionales de nuestra aplicación.

Revisión del flujo de filtros existente

En este momento, nuestros filtros solo funcionan al hacer clic en el botón “Buscar”. Esto significa que:

  1. El usuario cambia un select
  2. No pasa nada
  3. El usuario cambia otro select
  4. No pasa nada
  5. El usuario hace clic en “Buscar”
  6. Recién ahí se aplican todos los filtros

Este flujo no es óptimo para la experiencia de usuario. Vamos a mejorarlo.

Eliminación del botón “Buscar” y envío automático

La solución es simple: eliminar el botón de buscar y hacer que el formulario se envíe automáticamente cada vez que cambia un campo.

Transformación: eliminando el botón de submit

// src/components/SearchForm.jsx
export function SearchForm({ onSearch, onChangeText }) {
  const idText = useId()
  const idLocation = useId()

  // Ahora manejamos el cambio de cualquier campo
  const handleChange = (event) => {
    // IMPORTANTE: usar currentTarget, NO target
    const formData = new FormData(event.currentTarget)

    const filters = {
      search: formData.get(idText),
      location: formData.get(idLocation),
    }

    onSearch(filters)
  }

  const handleChangeText = (event) => {
    onChangeText(event.target.value)
    // También actualizamos los filtros cuando cambia el texto
    handleChange(event)
  }

  return (
    <form className="search-form" onChange={handleChange}>
      <input
        type="text"
        name={idText}
        id={idText}
        placeholder="Buscar..."
        onChange={handleChangeText}
      />

      <select name={idLocation} id={idLocation}>
        <option value="">Todas las ubicaciones</option>
        <option value="remoto">Remoto</option>
        <option value="presencial">Presencial</option>
      </select>
    </form>
  )
}

event.target vs event.currentTarget

Este es uno de los errores más comunes al trabajar con formularios en React. Es crucial entender la diferencia:

event.target

event.target es el elemento que lanzó el evento. En nuestro caso:

  • Si haces clic en el input → event.target es el <input>
  • Si cambias el select → event.target es el <select>
const handleChange = (event) => {
  console.log(event.target) // <input> o <select>, dependiendo de cuál cambió
}

event.currentTarget

event.currentTarget es el elemento que tiene el listener del evento. En nuestro caso, siempre es el <form>:

const handleChange = (event) => {
  console.log(event.currentTarget) // Siempre <form>
}

¿Por qué necesitamos currentTarget?

Cuando creamos un FormData, necesitamos pasarle el formulario completo, no solo el campo que cambió:

// ❌ INCORRECTO - Solo obtenemos los datos del input/select que cambió
const formData = new FormData(event.target)

// ✅ CORRECTO - Obtenemos todos los datos del formulario
const formData = new FormData(event.currentTarget)

Ejemplo visual del problema

<form onChange={handleChange}>
  <input name="search" />
  <select name="location">
    <option value="remoto">Remoto</option>
  </select>
</form>

Si cambias el select:

// Con event.target (INCORRECTO)
const formData = new FormData(event.target) // FormData del <select>
console.log(formData.get('search')) // null ❌
console.log(formData.get('location')) // null ❌ (los selects no funcionan así)

// Con event.currentTarget (CORRECTO)
const formData = new FormData(event.currentTarget) // FormData del <form>
console.log(formData.get('search')) // "..." ✅
console.log(formData.get('location')) // "remoto" ✅

Filtros funcionando en tiempo real

Después de corregir el currentTarget, el formulario filtra automáticamente cada vez que:

  • El usuario escribe en el input de búsqueda
  • El usuario cambia cualquier select
  • El usuario interactúa con cualquier campo del formulario

Ventajas de este enfoque

Resultados instantáneos - El usuario ve los cambios inmediatamente ✅ Menos clics - No necesita hacer clic en “Buscar” ✅ Más intuitivo - El comportamiento es más natural ✅ Unifica la experiencia - Texto y filtros funcionan de la misma manera

Necesidad de tener varias páginas

Hasta ahora, nuestra aplicación solo tiene una página. Pero una aplicación real necesita múltiples páginas:

  • Home (/) - Página principal con información del proyecto
  • Search (/search) - Página de búsqueda de trabajos
  • 404 - Página de error para rutas no encontradas

Problema identificado

Actualmente, todo el contenido está en App.jsx:

// src/App.jsx (ANTES)
function App() {
  // Todo el código de búsqueda y filtros
  return (
    <>
      <Header />
      <main>
        <SearchForm onSearch={handleSearch} onChangeText={handleChangeText} />
        <JobListings jobs={pagedResults} />
        <Pagination />
      </main>
      <Footer />
    </>
  )
}

Esto no escala bien cuando queremos agregar más páginas.

Reorganización del App y separación de layout

Vamos a reorganizar la estructura de la aplicación para que sea más modular:

Estructura de carpetas

src/
├── components/
│   ├── Header.jsx
│   ├── Footer.jsx
│   ├── SearchForm.jsx
│   ├── JobListings.jsx
│   └── Pagination.jsx
└── pages/
    ├── Home.jsx
    ├── Search.jsx
    └── NotFound.jsx

Crear la carpeta de páginas

mkdir src/pages

Mover la lógica de búsqueda a Search.jsx

// src/pages/Search.jsx
import { useState } from 'react'
import { SearchForm } from '../components/SearchForm'
import { JobListings } from '../components/JobListings'
import { Pagination } from '../components/Pagination'
import jobsData from '../data/jobs.json'

export function Search() {
  const [filters, setFilters] = useState({
    technology: '',
    location: '',
    experienceLevel: '',
  })

  // ... resto del código
}

Nuevo App.jsx simplificado

Ahora App.jsx solo contiene el layout (Header y Footer) y decide qué página mostrar:

// src/App.jsx
import { Header } from './components/Header'
import { Footer } from './components/Footer'
import { Home } from './pages/Home'
import { Search } from './pages/Search'
import { NotFound } from './pages/NotFound'

function App() {
  // Por ahora, mostramos siempre Search
  // En la próxima sección implementaremos el routing
  return (
    <>
      <Header />
      <Search />
      <Footer />
    </>
  )
}

export default App

Routing manual usando window.location.pathname

Ahora vamos a implementar un sistema básico de routing sin usar React Router. Usaremos window.location.pathname para detectar en qué ruta estamos.

¿Qué es window.location.pathname?

Es una propiedad del navegador que contiene la ruta actual:

// Si estás en: http://localhost:5173/
console.log(window.location.pathname) // "/"

// Si estás en: http://localhost:5173/search
console.log(window.location.pathname) // "/search"

// Si estás en: http://localhost:5173/about
console.log(window.location.pathname) // "/about"

Implementación del router manual

// src/App.jsx
import { Header } from './components/Header'
import { Footer } from './components/Footer'
import { Home } from './pages/Home'
import { Search } from './pages/Search'
import { NotFound } from './pages/NotFound'

function App() {
  // Obtenemos la ruta actual
  const { pathname } = window.location

  // Decidimos qué página mostrar según la ruta
  let Page

  if (pathname === '/') {
    Page = Home
  } else if (pathname === '/search') {
    Page = Search
  } else {
    Page = NotFound
  }

  return (
    <>
      <Header />
      <Page />
      <Footer />
    </>
  )
}

export default App

Versión alternativa más limpia

// src/App.jsx
import { Header } from './components/Header'
import { Footer } from './components/Footer'
import { Home } from './pages/Home'
import { Search } from './pages/Search'
import { NotFound } from './pages/NotFound'

function App() {
  const { pathname } = window.location

  return (
    <>
      <Header />
      {pathname === '/' && <Home />}
      {pathname === '/search' && <Search />}
      {pathname !== '/' && pathname !== '/search' && <NotFound />}
      <Footer />
    </>
  )
}

export default App

Creando la página 404

Cuando el usuario intenta acceder a una ruta que no existe, mostramos una página de error:

// src/pages/NotFound.jsx
export function NotFound() {
  return (
    <main className="not-found">
      <h1>404</h1>
      <h2>Página no encontrada</h2>
      <p>La página que buscas no existe.</p>
      <a href="/">Volver al inicio</a>
    </main>
  )
}

¡Aquí tienes un ejercicio! Tienes que estilar la página 404 para que tenga el mismo estilo que el resto de la aplicación.

Importando la Home original

Ahora vamos a recuperar el contenido original de la página Home que teníamos en la primera clase del curso.

Creando Home.jsx

// src/pages/Home.jsx
export function Home() {
  return (
    <main>
      <section className="hero">
        <h1>DevJobs</h1>
        <p>Encuentra el trabajo de tus sueños en desarrollo</p>
        <a href="/search" className="cta-button">
          Buscar trabajos
        </a>
      </section>

      <section className="features">
        <div className="feature">
          <h3>🚀 Oportunidades</h3>
          <p>Cientos de ofertas actualizadas diariamente</p>
        </div>
        <div className="feature">
          <h3>💼 Empresas top</h3>
          <p>Trabaja con las mejores empresas tecnológicas</p>
        </div>
        <div className="feature">
          <h3>🌍 Remoto</h3>
          <p>Encuentra trabajos remotos desde cualquier lugar</p>
        </div>
      </section>

      <section className="stats">
        <div className="stat">
          <h2>1,500+</h2>
          <p>Ofertas de trabajo</p>
        </div>
        <div className="stat">
          <h2>300+</h2>
          <p>Empresas</p>
        </div>
        <div className="stat">
          <h2>50+</h2>
          <p>Tecnologías</p>
        </div>
      </section>
    </main>
  )
}

Errores típicos al pasar HTML a JSX

Cuando migras HTML plano a JSX, ten cuidado con estos errores comunes:

1. Inputs sin cerrar

// ❌ HTML (no funciona en JSX)
<input type="text" name="search">

// ✅ JSX (self-closing)
<input type="text" name="search" />

2. Atributos con nombres diferentes

// ❌ HTML
<label for="search">Búsqueda</label>
<div class="container">

// ✅ JSX
<label htmlFor="search">Búsqueda</label>
<div className="container">

3. Atributos en camelCase

// ❌ HTML
<button onclick="handleClick()">

// ✅ JSX
<button onClick={handleClick}>

4. Comentarios diferentes

// ❌ HTML
<!-- Este es un comentario -->

// ✅ JSX
{/* Este es un comentario */}

5. Valores booleanos

// ❌ HTML
<input disabled>

// ✅ JSX
<input disabled={true} />
// o simplemente
<input disabled />

Mejorando navegación dentro del Header

Ahora que tenemos múltiples páginas, necesitamos que el Header permita navegar entre ellas.

Header con navegación

// src/components/Header.jsx
export function Header() {
  return (
    <header className="header">
      <div className="header-container">
        {/* Logo clicable que lleva a Home */}
        <a href="/" className="logo-link">
          <img src="/logo.png" alt="DevJobs" className="logo" />
          <h1>DevJobs</h1>
        </a>

        {/* Navegación */}
        <nav className="nav">
          <a href="/" className="nav-link">
            Inicio
          </a>
          <a href="/search" className="nav-link">
            Buscar trabajos
          </a>
        </nav>
      </div>
    </header>
  )
}

Problema con el routing manual

Nuestro router funciona, pero tiene un problema importante: cuando haces clic en los enlaces, la página se recarga completamente.

¿Por qué se recarga?

<a href="/search">Buscar trabajos</a>

Los enlaces HTML normales (<a href="...">) hacen que el navegador recargue la página completa. Esto:

  • ❌ Es lento
  • ❌ Pierde el estado de React
  • ❌ No se siente como una SPA (Single Page Application)

Solución temporal

Por ahora, nuestro router manual funciona así. En futuras clases aprenderemos a:

  1. Usar React Router - La solución profesional para routing en React
  2. Prevenir recargas - Mantener el estado sin recargar la página
  3. Sincronizar con la URL - Compartir URLs con filtros aplicados

Preparando la base para sincronizar filtros con la URL

Una característica avanzada que implementaremos más adelante es sincronizar los filtros con la URL:

http://localhost:5173/search?technology=react&location=remoto

Esto permite:

Compartir búsquedas - Envía un enlace con filtros aplicados

Marcar como favorito - Guarda búsquedas frecuentes

Historial del navegador - Vuelve a búsquedas anteriores con el botón “Atrás”

SEO friendly - Los motores de búsqueda pueden indexar las búsquedas

Próximos pasos

En las siguientes clases aprenderemos:

  • 🎯 React Router - Sistema profesional de routing
  • 🔗 URL Search Params - Sincronizar filtros con la URL
  • Navegación sin recarga - SPA real con Link y navigate
  • 📱 Rutas dinámicas - Páginas de detalle /jobs/:id

💡 Recuerda: Nuestro router manual es educativo, pero en producción siempre debes usar React Router o una librería similar.

Comparación: Router manual vs React Router

CaracterísticaRouter manualReact Router
Recarga la página❌ Sí✅ No
Mantiene el estado❌ No✅ Sí
Parámetros de URL❌ Manual✅ Automático
Rutas dinámicas❌ Difícil✅ Fácil
Navegación programática❌ Limitada✅ Completa
Historial del navegador⚠️ Básico✅ Completo
Producción❌ No recomendado✅ Recomendado

Conclusión: El router manual nos ayuda a entender cómo funciona el routing, pero React Router es la solución profesional.