Integrando navegación con formulario de búsqueda
En esta clase aprenderás a conectar un formulario de búsqueda con el router de tu aplicación SPA. Verás cómo manejar el evento de submit, extraer valores con FormData, construir URLs dinámicas con query params y reutilizar tu función de navegación personalizada.
El problema inicial
Actualmente en la Home tenemos un formulario con un input y un botón de búsqueda, pero no hace nada. El objetivo es hacerlo funcionar para que al enviar el formulario navegue a la página de búsqueda con el texto introducido como parámetro.
// Estado actual del formulario (no funcional)
function Home() {
return (
<form>
<input type="text" placeholder="Buscar..." />
<button disabled>
<svg>...</svg>
</button>
</form>
)
}
Limpiando el SVG del botón
Antes de empezar con la funcionalidad, es importante corregir los atributos del SVG para que sean compatibles con JSX.
Problema con los atributos SVG
En HTML los atributos usan kebab-case, pero en JSX debemos usar camelCase:
// ❌ Incorrecto (HTML)
<svg>
<path stroke-width="2" line-cap="round" />
</svg>
// ✅ Correcto (JSX)
<svg>
<path strokeWidth="2" lineCap="round" />
</svg>
Habilitando el botón de búsqueda
Para poder probar el formulario, necesitamos habilitar el botón eliminando el atributo disabled:
// ❌ Antes
<button disabled type="submit">
<svg>...</svg>
</button>
// ✅ Después
<button type="submit">
<svg>...</svg>
</button>
Cambios realizados:
- ❌
stroke-width→ ✅strokeWidth - ❌
stroke-linecap→ ✅strokeLinecap - ❌
stroke-linejoin→ ✅strokeLinejoin - Eliminamos el atributo
disableddel botón
Manejando el submit del formulario
Ahora vamos a implementar la lógica para capturar el evento de envío del formulario.
Crear el manejador de eventos
function Home() {
const handleSearch = (event) => {
// Prevenir el comportamiento por defecto (reload de página)
event.preventDefault()
// Aquí implementaremos la lógica
}
return (
<form onSubmit={handleSearch}>
<input type="text" placeholder="Buscar cursos..." />
<button type="submit">
<svg>...</svg>
</button>
</form>
)
}
¿Por qué event.preventDefault()?
En una aplicación web tradicional, cuando envías un formulario, el navegador:
- Recarga la página completa
- Pierde todo el estado de JavaScript
- Hace una petición al servidor
En una SPA (Single Page Application) queremos:
- ✅ Mantener la página sin recargar
- ✅ Conservar el estado
- ✅ Manejar la navegación con JavaScript
const handleSearch = (event) => {
event.preventDefault() // ← Evita el reload
// Ahora podemos controlar la navegación nosotros
}
Extrayendo el valor del input con FormData
Para obtener el texto que el usuario escribió en el input, usaremos la API de FormData.
Añadir el atributo name al input
Primero, el input necesita un atributo name para poder identificarlo:
<input
type="text"
name="search" // ← Importante
placeholder="Buscar cursos..."
/>
Usar FormData para extraer el valor
const handleSearch = (event) => {
event.preventDefault()
// Crear un objeto FormData desde el formulario
const formData = new FormData(event.target)
// Extraer el valor del input por su name
const searchTerm = formData.get('search')
console.log('Buscando:', searchTerm)
}
¿Por qué FormData en lugar de useState?
Podrías pensar: “¿Por qué no usar un estado controlado con useState?”
Opción 1: Estado controlado (más código)
function Home() {
const [searchTerm, setSearchTerm] = useState('')
const handleSearch = (event) => {
event.preventDefault()
console.log(searchTerm) // Usar el estado
}
return (
<form onSubmit={handleSearch}>
<input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} name="search" />
<button type="submit">Buscar</button>
</form>
)
}
Opción 2: FormData (menos código)
function Home() {
const handleSearch = (event) => {
event.preventDefault()
const formData = new FormData(event.target)
const searchTerm = formData.get('search')
console.log(searchTerm) // Obtener directamente
}
return (
<form onSubmit={handleSearch}>
<input name="search" />
<button type="submit">Buscar</button>
</form>
)
}
✅ FormData es más simple cuando:
- Solo necesitas el valor al enviar el formulario
- No necesitas validación en tiempo real
- Tienes múltiples campos
✅ useState es mejor cuando:
- Necesitas validación mientras el usuario escribe
- Quieres mostrar el valor en otros componentes
- Necesitas deshabilitar el botón si el campo está vacío
Construyendo la URL de destino
Ahora necesitamos construir la URL a la que queremos navegar según si el usuario escribió algo o no.
Lógica de la URL
const handleSearch = (event) => {
event.preventDefault()
const formData = new FormData(event.target)
const searchTerm = formData.get('search')
// Construir la URL según si hay texto o no
let targetUrl = '/search'
if (searchTerm) {
targetUrl += `?text=${searchTerm}`
}
console.log('Navegar a:', targetUrl)
}
Ejemplos de URLs generadas:
- Usuario escribe “programador” →
/search?text=programador - Usuario escribe “React de Dev” →
/search?text=React de Dev - Usuario deja vacío →
/search
Problema con espacios y caracteres especiales
Si el usuario escribe “React de Dev”, la URL quedaría:
/search?text=React de Dev ← ¡URL inválida!
Necesitamos codificar la URL:
const handleSearch = (event) => {
event.preventDefault()
const formData = new FormData(event.target)
const searchTerm = formData.get('search')
let targetUrl = '/search'
if (searchTerm) {
// Codificar el texto para que sea seguro en la URL
const encodedTerm = encodeURIComponent(searchTerm)
targetUrl += `?text=${encodedTerm}`
}
console.log('Navegar a:', targetUrl)
}
Ahora sí funciona correctamente:
- “React de Dev” →
/search?text=React%20de%20Dev✅ - “C++” →
/search?text=C%2B%2B✅ - “¿Qué es React?” →
/search?text=%C2%BFQu%C3%A9%20es%20React%3F✅
¿Qué hace encodeURIComponent?
Convierte caracteres especiales a su representación segura para URLs:
| Carácter | Codificado | Razón |
|---|---|---|
| espacio | %20 | Los espacios no son válidos en URL |
+ | %2B | Tiene significado especial |
? | %3F | Delimita query params |
& | %26 | Separa query params |
= | %3D | Asigna valores en query params |
# | %23 | Indica fragmento de URL |
Reutilizando navigateTo del custom hook useRouter
En lugar de usar directamente window.history.pushState, vamos a reutilizar el custom hook useRouter que creamos en la clase anterior.
¿Por qué no usar window.history.pushState directamente?
Si hiciéramos esto:
const handleSearch = (event) => {
event.preventDefault()
// ...construcción de URL...
// ❌ Problema: Solo actualiza la URL, no renderiza la vista
window.history.pushState({}, '', targetUrl)
}
Problemas:
- ❌ La URL cambia, pero la vista no se actualiza
- ❌ Los listeners del router no se disparan
- ❌ El estado del componente no cambia
- ❌ Código duplicado de la lógica del router
Solución: Usar el hook useRouter
En la clase anterior creamos el custom hook useRouter que encapsula toda la lógica del router:
// hooks/useRouter.js
import { useState, useEffect } from 'react'
export function useRouter() {
const [currentPath, setCurrentPath] = useState(window.location.pathname)
useEffect(() => {
const handleLocationChange = () => {
setCurrentPath(window.location.pathname)
}
window.addEventListener('popstate', handleLocationChange)
return () => window.removeEventListener('popstate', handleLocationChange)
}, [])
const navigateTo = (path) => {
window.history.pushState({}, '', path)
setCurrentPath(path)
}
return {
currentPath,
navigateTo,
}
}
Este hook nos proporciona la función navigateTo que:
- ✅ Actualiza la URL en el navegador
- ✅ Actualiza el estado del componente
- ✅ Dispara el re-render necesario
- ✅ Mantiene la lógica del router en un solo lugar
Implementación completa con useRouter
Ahora usamos el hook en nuestro componente Home:
import { useRouter } from './hooks/useRouter'
function Home() {
// Obtenemos navigateTo del custom hook
const { navigateTo } = useRouter()
const handleSearch = (event) => {
event.preventDefault()
const formData = new FormData(event.target)
const searchTerm = formData.get('search')
let targetUrl = '/search'
if (searchTerm) {
const encodedTerm = encodeURIComponent(searchTerm)
targetUrl += `?text=${encodedTerm}`
}
// ✅ Usar navigateTo del hook
navigateTo(targetUrl)
}
return (
<form onSubmit={handleSearch}>
<input type="text" name="search" placeholder="Buscar cursos..." />
<button type="submit">
<svg ... />
</button>
</form>
)
}
Ventajas de usar el hook:
- ✅ Reutilización: Cualquier componente puede usar
useRouter - ✅ Separación de responsabilidades: La lógica del router está encapsulada
- ✅ Consistencia: Todos los componentes navegan de la misma manera
- ✅ Mantenibilidad: Cambios en el router solo en un lugar
Probando la funcionalidad
Vamos a probar diferentes escenarios:
Caso 1: Búsqueda con texto
- Usuario escribe “programador” en el input
- Usuario presiona “Buscar” o Enter
- La página navega a
/search?text=programador - El router renderiza la vista de búsqueda
- La URL en el navegador se actualiza correctamente
Caso 2: Búsqueda vacía
- Usuario deja el input vacío
- Usuario presiona “Buscar”
- La página navega a
/search(sin query params) - El router renderiza la vista de búsqueda
- Mostraría todos los cursos disponibles
Caso 3: Búsqueda con espacios
- Usuario escribe “React de Dev”
- Usuario presiona “Buscar”
- La página navega a
/search?text=React%20de%20Dev - El router renderiza la vista de búsqueda
- La URL está correctamente codificada
Caso 4: Navegación con botón “Atrás”
- Usuario hace una búsqueda
- Usuario presiona el botón “Atrás” del navegador
- El router detecta el cambio (evento
popstate) - La vista vuelve a la Home
- Todo funciona correctamente ✅
Código completo del componente Home
import { useRouter } from './hooks/useRouter'
function Home() {
// Usar el custom hook para obtener la función de navegación
const { navigateTo } = useRouter()
const handleSearch = (event) => {
event.preventDefault()
// Extraer el valor del input usando FormData
const formData = new FormData(event.target)
const searchTerm = formData.get('search')
// Construir la URL de destino
let targetUrl = '/search'
if (searchTerm) {
const encodedTerm = encodeURIComponent(searchTerm)
targetUrl += `?text=${encodedTerm}`
}
// Navegar usando navigateTo del hook
navigateTo(targetUrl)
}
return (
<div className="home">
<h1>Encuentra el mejor curso para ti</h1>
<form onSubmit={handleSearch}>
<input type="text" name="search" placeholder="Buscar cursos..." className="search-input" />
<button type="submit" className="search-button">
<svg ... />
</button>
</form>
</div>
)
}
export default Home
Próximos pasos
En las siguientes clases veremos:
- Sincronizar filtros con la URL: Los filtros de la página de búsqueda también actualizarán los query params
- Leer query params: Extraer
?text=programadorde la URL para mostrar los resultados - Filtrado dinámico: Aplicar los filtros de búsqueda a los cursos
- Mantener estado en la URL: Para que los usuarios puedan compartir búsquedas específicas
Conceptos clave aprendidos
| Concepto | Descripción |
|---|---|
event.preventDefault() | Evita el comportamiento por defecto del formulario |
FormData | API para extraer valores de formularios fácilmente |
encodeURIComponent | Codifica texto para que sea seguro en URLs |
| Query params | Parámetros en la URL: ?text=valor&filtro=otro |
navigateTo | Reutilizar lógica del router en lugar de duplicar |
Atributo name | Identifica inputs para poder extraer sus valores |
| JSX attributes | Usar camelCase en lugar de kebab-case para SVG/HTML |
Conclusión
En esta clase has aprendido a:
- ✅ Corregir atributos SVG para que funcionen con JSX
- ✅ Manejar el evento
submitde formularios - ✅ Usar
event.preventDefault()para controlar el comportamiento - ✅ Extraer valores de formularios con FormData
- ✅ Construir URLs dinámicas con query params
- ✅ Codificar texto para URLs con
encodeURIComponent - ✅ Reutilizar la función
navigateTodel router - ✅ Integrar formularios con navegación SPA
Ahora tu formulario de búsqueda está completamente funcional y se integra correctamente con el sistema de routing de tu aplicación. En las próximas clases implementaremos la lógica para leer estos parámetros y mostrar los resultados filtrados.