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
URLSearchParamsmanualmente para leer y escribir los parámetros - ✅ Si navegas a
?technology=javascripto?technology=pythony 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.searchtú mismo - Crear instancias de
URLSearchParamsa 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 unURLSearchParams, 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.searchtú mismo - Estás creando
URLSearchParamsa 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
localStorageen 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
URLSearchParamsen 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
setSearchParamsrecibe una función con los parámetros anteriores- Dentro creas un
URLSearchParamsnuevo 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íficoappend: 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
- Cambia tu código para usar
appenden lugar deset - Actualiza los filtros varias veces
- Observa cómo la URL se llena de duplicados
- Corrígelo usando
set - Verifica que ahora funciona correctamente
¡Resumen final!
En esta clase hemos aprendido:
- ✅ Por qué usar
useSearchParamsen lugar deURLSearchParamsmanual - ✅ 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
appendyset - ✅ 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.