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:
- El usuario cambia un select
- No pasa nada
- El usuario cambia otro select
- No pasa nada
- El usuario hace clic en “Buscar”
- 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.targetes el<input> - Si cambias el select →
event.targetes 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:
- Usar React Router - La solución profesional para routing en React
- Prevenir recargas - Mantener el estado sin recargar la página
- 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
Linkynavigate - 📱 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ística | Router manual | React 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.