Hook useSearchParams de React Router

Contexto: el buscador que ya funciona

En este punto del proyecto ya tienes un buscador que funciona perfectamente:

  • ✅ Sincroniza el texto de búsqueda y los filtros con la URL
  • ✅ Usa URLSearchParams manualmente para leer y escribir los parámetros
  • ✅ Si navegas a ?technology=javascript o ?technology=python y recargas, el filtro se mantiene correctamente

Pero hay un problema: todo esto requiere bastante código repetitivo y poco ergonómico.

Tienes que:

  • Leer window.location.search tú mismo
  • Crear instancias de URLSearchParams a mano
  • Construir la URL manualmente con navigate
  • Repetir este patrón cada vez que necesitas un nuevo filtro

Es momento de dejar de reinventar la rueda y usar lo que React Router ya nos ofrece.

El hook useSearchParams: la solución elegante

React Router incluye un hook diseñado específicamente para esto: useSearchParams.

Este hook nos permite:

  • Leer fácilmente los parámetros de la URL
  • Actualizarlos sin tener que construir la URL manualmente
  • Mantener toda la lógica de query params encapsulada

Y de paso, vamos a ver un patrón importante para inicializar el estado de forma eficiente en React.

Importación básica

import { useSearchParams } from 'react-router'

Qué devuelve

const [searchParams, setSearchParams] = useSearchParams()

Esto es similar a un useState, pero para la query string:

  • searchParams: Se comporta como un URLSearchParams, con métodos como .get(), .has(), etc.
  • setSearchParams: La función que usaremos para actualizar los parámetros de la URL

El estado inicial: antes de useSearchParams

Recordemos cómo estaba el código antes de introducir useSearchParams.

Por ejemplo, en tu custom hook useSearchForm:

import { useState } from 'react'

function getInitialTextFromURL() {
  const params = new URLSearchParams(window.location.search)
  const textFromURL = params.get('text') ?? ''
  return textFromURL
}

export function useSearchForm() {
  const [textToFilter, setTextToFilter] = useState(getInitialTextFromURL())

  // ... resto del código
}

La demostración

Si escribes “JavaScript” en el buscador y recargas la página, el buscador sigue mostrando “JavaScript” porque el estado se inicializa leyendo la URL.

Esto funciona, pero:

  • Estás leyendo window.location.search tú mismo
  • Estás creando URLSearchParams a mano
  • Tendrás que repetir este patrón en más sitios si necesitas más filtros

Leer parámetros con useSearchParams

Ahora vamos a sustituir la lectura manual por el hook.

Antes (manual)

const params = new URLSearchParams(window.location.search)
const initialText = params.get('text') ?? ''

Después (con el hook)

const [searchParams] = useSearchParams()
const initialText = searchParams.get('text') ?? ''

Mucho más limpio y sin tener que acceder a window.location.

Inicialización eficiente del estado

Aquí es donde hacemos una pausa para hablar de algo que va más allá de React Router: cómo inicializar correctamente un estado en React.

Dos formas de inicializar un useState

Forma 1: Pasando un valor directamente

const [textToFilter, setTextToFilter] = useState(searchParams.get('text') ?? '')

Problema: Este código se evalúa en cada render.

Forma 2: Pasando una función (lazy initialization)

const [textToFilter, setTextToFilter] = useState(() => {
  console.log('Inicializar estado de textToFilter')
  return searchParams.get('text') ?? ''
})

Ventaja: React llama a esta función solo una vez, cuando se crea el estado inicial.

Demostración con console.log

Vamos a verlo en acción:

const [textToFilter, setTextToFilter] = useState(() => {
  console.log('Inicializar estado de textToFilter')
  return searchParams.get('text') ?? ''
})

Si abres las DevTools y empiezas a:

  • Escribir en el input
  • Cambiar filtros
  • Navegar entre tecnologías

El console.log solo aparece una vez, aunque el componente se renderice muchas veces.

Comparación con la versión sin función

Ahora compara con esta versión:

const initialText = searchParams.get('text') ?? ''

console.log('Hola')

const [textToFilter, setTextToFilter] = useState(initialText)

Aquí el console.log('Hola') se ejecuta:

  • Cada vez que el componente se renderiza
  • Porque no está dentro de una función que React ejecute solo al inicializar

¿Por qué importa?

Esto se nota especialmente cuando:

  • Lees de localStorage en la inicialización
  • Parseas muchos parámetros de la URL
  • Haces cálculos pesados para obtener el estado inicial

Regla de oro

Siempre que la inicialización de un estado haga trabajo extra o pueda ser costosa, envuélvelo en una función y pásala a useState. Así te aseguras de que solo se ejecuta una vez.

Actualizar los parámetros con setSearchParams

Hasta ahora solo estábamos leyendo la URL. Ahora toca la parte interesante: actualizarla.

El código manual que teníamos antes

Recordemos cómo actualizábamos los filtros antes:

function updateFiltersInURL({ text, technology, remote }) {
  const params = new URLSearchParams(window.location.search)

  if (text) params.set('text', text)
  else params.delete('text')

  if (technology) params.set('technology', technology)
  else params.delete('technology')

  if (remote) params.set('remote', 'true')
  else params.delete('remote')

  navigate(`?${params.toString()}`)
}

Este código:

  • ✅ Funciona
  • ❌ Mezcla lógica de filtros con lógica de navegación
  • ❌ Repite la misma idea de URLSearchParams en varios sitios

La versión con setSearchParams

Ahora reescribimos esto usando el hook:

import { useSearchParams } from 'react-router'

export function useSearchForm() {
  const [searchParams, setSearchParams] = useSearchParams()

  const [textToFilter, setTextToFilter] = useState(() => {
    return searchParams.get('text') ?? ''
  })

  const updateFiltersInURL = ({ text, technology, remote }) => {
    setSearchParams((prevParams) => {
      const params = new URLSearchParams(prevParams)

      if (text) params.set('text', text)
      else params.delete('text')

      if (technology) params.set('technology', technology)
      else params.delete('technology')

      if (remote) params.set('remote', 'true')
      else params.delete('remote')

      return params
    })
  }

  // ... resto del código
}

Puntos clave

  • setSearchParams recibe una función con los parámetros anteriores
  • Dentro creas un URLSearchParams nuevo a partir de los anteriores
  • Modificas solo lo que necesitas
  • Devuelves los nuevos parámetros y React Router actualiza la URL automáticamente

Ya no haces navigate ni montas la URL tú. setSearchParams hace ese trabajo por ti.

Bug típico: append vs set

Aquí viene un error muy común que es importante que veas en acción.

El error con append

Imagina que usas append en lugar de set:

setSearchParams((prevParams) => {
  const params = new URLSearchParams(prevParams)

  params.append('text', textToFilter)
  params.append('technology', technology)
  // ... más append

  return params
})

Si actualizas los filtros varias veces, la URL empieza a acumular parámetros duplicados:

?text=react&text=react&text=react&technology=node&technology=node

¿Por qué pasa esto?

append añade siempre un nuevo valor, no reemplaza el existente.

Cada vez que llamas a la función, vuelves a meter el parámetro una vez más.

La solución: usar set

Hay dos formas de arreglarlo:

Opción 1: Usar set en lugar de append

params.set('text', textToFilter) // Reemplaza el valor existente

Opción 2: Reconstruir la query desde cero

setSearchParams(() => {
  const params = new URLSearchParams()

  if (textToFilter) params.set('text', textToFilter)
  if (technology) params.set('technology', technology)
  if (remote) params.set('remote', 'true')

  return params
})

Cuándo usar cada uno

  • set: Cuando quieres actualizar o añadir un parámetro específico
  • append: Cuando quieres múltiples valores para la misma clave (raro en filtros)
  • Reconstruir desde cero: Cuando quieres tener control total sobre qué parámetros existen

Comparación final: antes vs después

Antes (manual)

// ❌ Leer la URL manualmente
const params = new URLSearchParams(window.location.search)
const initialText = params.get('text') ?? ''

// ❌ Inicializar estado en cada render
const [textToFilter, setTextToFilter] = useState(initialText)

// ❌ Actualizar URL con navigate
function updateFiltersInURL({ text, technology, remote }) {
  const params = new URLSearchParams(window.location.search)
  // ... lógica de set/delete
  navigate(`?${params.toString()}`)
}

Después (con useSearchParams)

// ✅ Leer la URL con el hook
const [searchParams, setSearchParams] = useSearchParams()

// ✅ Inicializar estado con función (solo una vez)
const [textToFilter, setTextToFilter] = useState(() => {
  return searchParams.get('text') ?? ''
})

// ✅ Actualizar URL con setSearchParams
const updateFiltersInURL = ({ text, technology, remote }) => {
  setSearchParams((prevParams) => {
    const params = new URLSearchParams(prevParams)
    // ... lógica de set/delete
    return params
  })
}

Ejercicios para practicar

Ejercicio 1: Añadir un nuevo filtro

Añade un nuevo filtro (por ejemplo, seniority con valores “junior”, “mid”, “senior”) que:

  • Se sincronice con la URL usando useSearchParams
  • Se inicialice correctamente al cargar la página
  • Se actualice junto con los demás filtros

Ejercicio 2: Convertir otras inicializaciones

Si tienes otras inicializaciones de estado que usen URLSearchParams o localStorage, conviértelas a la versión con función en useState:

// ❌ Antes
const [user, setUser] = useState(JSON.parse(localStorage.getItem('user')))

// ✅ Después
const [user, setUser] = useState(() => {
  return JSON.parse(localStorage.getItem('user'))
})

Ejercicio 3: Reproducir y corregir el bug de append

  1. Cambia tu código para usar append en lugar de set
  2. Actualiza los filtros varias veces
  3. Observa cómo la URL se llena de duplicados
  4. Corrígelo usando set
  5. Verifica que ahora funciona correctamente

¡Resumen final!

En esta clase hemos aprendido:

  • Por qué usar useSearchParams en lugar de URLSearchParams manual
  • Cómo leer parámetros de la URL con searchParams.get()
  • Inicialización eficiente del estado con funciones en useState
  • Cómo actualizar la URL con setSearchParams
  • La diferencia crítica entre append y set
  • Un hook nuevo de React Router que simplifica código existente

Has limpiado código que ya existía, has introducido una buena práctica de rendimiento en React, y has aprendido a usar un hook clave de React Router.

Tu buscador sigue funcionando igual, pero ahora el código es más limpio, más profesional y más fácil de mantener.