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 disabled del 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:

  1. Recarga la página completa
  2. Pierde todo el estado de JavaScript
  3. Hace una petición al servidor

En una SPA (Single Page Application) queremos:

  1. ✅ Mantener la página sin recargar
  2. ✅ Conservar el estado
  3. ✅ 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ácterCodificadoRazón
espacio%20Los espacios no son válidos en URL
+%2BTiene significado especial
?%3FDelimita query params
&%26Separa query params
=%3DAsigna valores en query params
#%23Indica 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:

  1. ❌ La URL cambia, pero la vista no se actualiza
  2. ❌ Los listeners del router no se disparan
  3. ❌ El estado del componente no cambia
  4. ❌ 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:

  1. ✅ Actualiza la URL en el navegador
  2. ✅ Actualiza el estado del componente
  3. ✅ Dispara el re-render necesario
  4. ✅ 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

  1. Usuario escribe “programador” en el input
  2. Usuario presiona “Buscar” o Enter
  3. La página navega a /search?text=programador
  4. El router renderiza la vista de búsqueda
  5. La URL en el navegador se actualiza correctamente

Caso 2: Búsqueda vacía

  1. Usuario deja el input vacío
  2. Usuario presiona “Buscar”
  3. La página navega a /search (sin query params)
  4. El router renderiza la vista de búsqueda
  5. Mostraría todos los cursos disponibles

Caso 3: Búsqueda con espacios

  1. Usuario escribe “React de Dev”
  2. Usuario presiona “Buscar”
  3. La página navega a /search?text=React%20de%20Dev
  4. El router renderiza la vista de búsqueda
  5. La URL está correctamente codificada

Caso 4: Navegación con botón “Atrás”

  1. Usuario hace una búsqueda
  2. Usuario presiona el botón “Atrás” del navegador
  3. El router detecta el cambio (evento popstate)
  4. La vista vuelve a la Home
  5. 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:

  1. Sincronizar filtros con la URL: Los filtros de la página de búsqueda también actualizarán los query params
  2. Leer query params: Extraer ?text=programador de la URL para mostrar los resultados
  3. Filtrado dinámico: Aplicar los filtros de búsqueda a los cursos
  4. Mantener estado en la URL: Para que los usuarios puedan compartir búsquedas específicas

Conceptos clave aprendidos

ConceptoDescripción
event.preventDefault()Evita el comportamiento por defecto del formulario
FormDataAPI para extraer valores de formularios fácilmente
encodeURIComponentCodifica texto para que sea seguro en URLs
Query paramsParámetros en la URL: ?text=valor&filtro=otro
navigateToReutilizar lógica del router en lugar de duplicar
Atributo nameIdentifica inputs para poder extraer sus valores
JSX attributesUsar 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 submit de 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 navigateTo del 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.