Sincronización de la URL con el buscador
Introducción al problema
En esta clase partimos de un buscador que ya tiene:
- Filtros (tecnología, ubicación, tipo, experiencia)
- Paginación
- Texto de búsqueda
- Llamadas a la API cuando cambian estos valores
Pero falta algo importante: la URL no refleja nada de este estado.
- Si cambias filtros, la URL no cambia
- Si cambias de página, la URL no cambia
- Si refrescas, pierdes todo y vuelves al estado inicial
La idea es que la URL sea un reflejo de la búsqueda actual, para poder:
- Compartir el link con otra persona
- Abrirlo en otra pestaña y que cargue igual
- Refrescar la página sin perder filtros ni paginación
Situación actual en el componente Search
En el componente Search ya tenemos un efecto que se encarga de llamar a la API cuando cambian los filtros, el texto o la página actual:
useEffect(() => {
// cada vez que cambian los filtros, el texto o la página
// hacemos la llamada a la API
fetchJobs({ filters, textToFilter, currentPage })
}, [filters, textToFilter, currentPage])
Esto está bien para los datos, pero:
- No actualiza la URL
- Si el usuario refresca, el estado se reinicia con valores por defecto
La solución será añadir otro efecto dedicado a sincronizar la URL.
Por qué sincronizar con la URL
Ventajas de sincronizar el estado con la URL:
Links compartibles
Puedes copiar la URL y enviar exactamente la misma búsqueda a otra persona.
Por ejemplo:
/search?text=React&technology=javascript&type=remote&page=2
Refrescar sin perder el estado
El usuario puede hacer F5 y mantener filtros, texto y página actual.
Mejor experiencia de navegación
El historial del navegador tiene sentido. Botón de atrás y adelante respetan los filtros que se han usado.
Multi pestañas
Abrir la misma URL en otra pestaña muestra el mismo estado.
Estrategia general
Vamos a dividir la sincronización en dos partes:
- Escribir en la URL cuando cambie el estado: añadimos un
useEffectque escucha cambios en filtros, texto y página, construye los query params y navega a la nueva URL - Leer la URL cuando cargue la página: usamos inicialización perezosa en
useStatepara inicializar el estado desde los parámetros de la URL
Paso 1: Efecto para reflejar el estado en la URL
En el componente Search añadimos un segundo efecto. Mismas dependencias que el anterior, pero con otro propósito: actualizar la URL.
useEffect(() => {
const params = new URLSearchParams()
if (textToFilter) {
params.set('text', textToFilter)
}
if (filters.technology) {
params.set('technology', filters.technology)
}
if (filters.location) {
params.set('location', filters.location)
}
if (filters.experience) {
params.set('experience', filters.experience)
}
if (currentPage > 1) {
params.set('page', String(currentPage))
}
const paramsString = params.toString()
const basePath = window.location.pathname
const newUrl = paramsString ? `${basePath}?${paramsString}` : basePath
navigateTo(newUrl)
}, [filters, textToFilter, currentPage, navigateTo])
Puntos clave:
- Usamos
URLSearchParamspara construir los query params - Solo añadimos parámetros si tienen valor
- La página solo se añade si es mayor que 1, porque la 1 es el valor por defecto
- Usamos
window.location.pathnameen lugar de escribir a mano/search
Por qué window.location.pathname y no /search
No es buena idea hardcodear /search:
- Si mañana cambiamos la ruta a
/jobs, habría que buscar y reemplazar en el código - Con
window.location.pathnameusamos el path actual de forma automática
Usando el router casero (navigateTo)
En este proyecto tenemos un router propio con un hook useRouter que expone navigateTo:
const { navigateTo } = useRouter()
navigateTo(newUrl):
- Actualiza la barra de direcciones
- Lanza un evento para notificar que la URL ha cambiado
- La app sigue funcionando como una Single Page Application
Por eso el efecto depende también de navigateTo y el linter nos obliga a incluirlo en el array de dependencias, aunque sepamos que no va a cambiar.
Paso 2: El problema al refrescar la página
Después de este paso:
- Cambias filtros, página o texto → la URL se actualiza
- Los resultados se actualizan
Pero si refrescas la página:
- Los filtros se pierden
- La página vuelve a 1
- El texto de búsqueda se vacía
¿Por qué? Porque el estado inicial está definido con valores fijos:
const [filters, setFilters] = useState({})
const [currentPage, setCurrentPage] = useState(1)
const [textToFilter, setTextToFilter] = useState('')
Da igual lo que tenga la URL, siempre arrancamos desde cero.
Hay que hacer que el estado inicial dependa de la URL.
Paso 3: Inicializar el estado leyendo los parámetros de la URL
Inicializar textToFilter desde ?text=…
Usamos la forma de useState que recibe una función para inicializar el valor solo una vez:
const [textToFilter, setTextToFilter] = useState(() => {
const params = new URLSearchParams(window.location.search)
const text = params.get('text')
return text ?? ''
})
Lo que hacemos:
window.location.searchcontiene la parte?text=React&...params.get('text')devuelve el valor onull- Si no hay valor, usamos
''como valor por defecto
Ahora:
- Si entras en
/search?text=React,textToFilterempieza siendo “React” - Las llamadas a la API ya arrancan con ese texto
Inicializar currentPage desde ?page=…
Queremos leer page de la URL, validar el valor y transformarlo en número:
const [currentPage, setCurrentPage] = useState(() => {
const params = new URLSearchParams(window.location.search)
const pageParam = params.get('page')
if (!pageParam) return 1
const page = Number(pageParam)
if (Number.isNaN(page) || page < 1) {
return 1
}
return page
})
Detalles importantes:
- Todo lo que viene de la URL es texto
- Hay que transformarlo con
Number(...) - Validamos:
- Si no hay parámetro, usamos 1
- Si no es numérico o es menor que 1, usamos 1
- Podríamos añadir más validaciones (limitar a un máximo, etc.)
Inicializar los filtros desde la URL
Hacemos lo mismo con el estado de filtros:
const [filters, setFilters] = useState(() => {
const params = new URLSearchParams(window.location.search)
const nextFilters = {}
const technology = params.get('technology')
if (technology) {
nextFilters.technology = technology
}
const location = params.get('location')
if (location) {
nextFilters.location = location
}
const type = params.get('type')
if (type) {
nextFilters.type = type
}
const experience = params.get('experience')
if (experience) {
nextFilters.experience = experience
}
return nextFilters
})
Así:
- Entrar en una URL como
/search?technology=javascript&type=remote&experience=seniorinicializa el estado con esos filtros - El efecto de llamada a la API usará esta información desde el primer render
Paso 4: Sincronizar el input de búsqueda con el valor inicial
Hay un detalle importante: la URL y los resultados son correctos, pero el input de búsqueda no muestra el texto hasta que escribes.
Solución:
- El hook que maneja la página de búsqueda expone también el
textToFilter - La página
SearchPagese lo pasa al formulario como prop inicial - El formulario usa esa prop como valor inicial del input
Exponer textToFilter desde el hook
function useSearchPage() {
const [textToFilter, setTextToFilter] = useState(/* inicialización desde la URL */)
// ...más estado y lógica
return {
textToFilter,
setTextToFilter,
// otros valores
}
}
Pasar el valor inicial al SearchForm
export function SearchPage() {
const {
textToFilter,
filters,
currentPage,
// ...
} = useSearchPage()
return (
<SearchForm
initialTextToFilter={textToFilter}
// resto de props
/>
)
}
Usar initialTextToFilter dentro del formulario
En el SearchForm:
export function SearchForm({ initialTextToFilter, onTextFilter }) {
const [text, setText] = useState(initialTextToFilter)
const handleChange = (event) => {
const value = event.target.value
setText(value)
onTextFilter(value)
}
return (
<input type="text" value={text} onChange={handleChange} placeholder="Buscar por texto..." />
)
}
Con esto:
- El input arranca mostrando el texto que viene de la URL
- Los cambios que hace el usuario se propagan al estado y a la URL
Comportamiento final
Al terminar esta clase, deberías poder:
Cambiar filtros, texto y página:
- La URL se actualiza con los query params correctos
Refrescar la página:
- Los resultados se mantienen
- El input de texto muestra el valor correcto
- Los select de filtros reflejan la selección
- La paginación respeta el
pagede la URL
Compartir la URL:
- Otra persona verá la misma búsqueda
Abrir la URL en otra pestaña:
- El estado inicial coincidirá con lo que está codificado en la URL
Resumen y buenas prácticas
Ideas clave de esta clase:
- Puedes tener varios useEffect con las mismas dependencias pero responsabilidades distintas: uno para datos, otro para sincronizar la URL
- Usa URLSearchParams para construir y leer query params de forma más limpia
- Evita hardcodear rutas como
/searchcuando puedas usarwindow.location.pathname - Inicializar estado desde la URL con funciones en useState evita recomputar en cada render
- Valida siempre lo que viene de la URL: números que no son números, valores negativos, parámetros vacíos
Con esto, el buscador pasa de ser un componente aislado a ser una búsqueda navegable, compartible y robusta, integrada con el navegador.
¡Y ahora vamos a usar React Router para subir de nivel nuestra aplicación!