react-router HashRouter support?
Context
What's your version of nuqs?
-> https://pkg.pr.new/nuqs@807
What framework are you using?
- ✅ React Router
Which version of your framework are you using?
-> 7.0.2
Description
For some reason, we can't use BrowserRouter in RR7, only HashRouter allowed. But useQueryState always append the whole hash part.
Expect:
http://localhost:5173/#/form
⇓
http://localhost:5173/#/form?keyword=kw
⇓
http://localhost:5173/#/form?keyword=kw1
⇓
http://localhost:5173/#/form?keyword=kw12
⇓
http://localhost:5173/#/form?keyword=kw123
Actual:
http://localhost:5173/#/form
⇓
http://localhost:5173/#/form?keyword=kw#/form
⇓
http://localhost:5173/#/form?keyword=kw1#/form?keyword=kw#/form
⇓
http://localhost:5173/#/form?keyword=kw12#/form?keyword=kw1#/form?keyword=kw#/form
⇓
http://localhost:5173/#/form?keyword=kw123#/form?keyword=kw12#/form?keyword=kw1#/form?keyword=kw#/form
Reproduction
https://github.com/Jungzl/react-router-nuqs-hash-issue
- go to '/form'
- type something, press Enter
- see url
Thanks, I'm going to mark this one as a feature request to support the HashRouter in RRv{6,7}, as the adapters have only been designed for search params in mind (the BrowserRouter).
Fortunately, the change in #800 should make it possible.
See also #206.
supp future self!
Looks like hash router was working fine with nuqs v2.2.1 and react-router v6, but after upgrading to 2.3.1 it is not working anymore. Is this a known issue? @franky47 looks like a breaking change to me
@gsaandy what about 2.2.2 and 2.2.3? I'm trying to pinpoint what change could have broken it, though my bet is on #811 (so those versions should work, as 811 shipped in 2.3.0).
@gsaandy what about 2.2.2 and 2.2.3? I'm trying to pinpoint what change could have broken it, though my bet is on #811 (so those versions should work, as 811 shipped in 2.3.0).
@franky47 - with2.2.3 it seems to be working fine, the problem is with 2.3.x
Also, irrespective of this version and issue, another issue with react router adapter is that we cannot use multiple useQueryState or useQueryStates in a page tree, one would override the other.
const [hello, setHello] = useQueryState('hello', { defaultValue: '' });
const [count, setCount] = useQueryState(
'count',
parseAsInteger.withDefault(0)
);
see https://stackblitz.com/edit/vitejs-vite-ejjuoacj?file=src%2Fmain.tsx
@franky47 - is this taken care in the latest version?
No, HashRouter support will likely need its own adapter due to the different storage mechanism being used (for example, all options and tests involving server-side rendering would fail, due to the hash never being sent to the server).
Contributions are welcome to provide this as a community-based adapter in the mean time (based on the implementation in 2.2.3).
Does not use react router, hash router, or browser router, but does routing via hashes instead of query params.
import { unstable_createAdapterProvider as createAdapterProvider } from "nuqs/adapters/custom";
import { FC, type PropsWithChildren, useEffect, useState } from "react";
export type { default } from "react";
/**
* Hook that Nuqs will call to read & write URL state.
*/
function useHashAdapter() {
// read current state from everything after the “#”
const rawHash = window.location.hash.slice(1);
const [hashParams, setHashParams] = useState(new URLSearchParams(rawHash));
// write new state back into the hash fragment
function updateUrl(updated: URLSearchParams) {
const { pathname, search } = window.location;
const hash = decodeURIComponent(updated.toString());
const url = `${pathname}${search}#${hash}`;
window.history.pushState(null, "", url);
setHashParams(updated);
}
// expose a snapshot of the current params
function getSearchParamsSnapshot() {
const raw = window.location.hash.slice(1);
return new URLSearchParams(raw);
}
// sync state when user navigates via browser controls or external script
useEffect(() => {
function handleHashChange() {
const rawHash = window.location.hash.slice(1);
setHashParams(new URLSearchParams(rawHash));
}
window.addEventListener("hashchange", handleHashChange);
return () => window.removeEventListener("hashchange", handleHashChange);
}, []);
return { searchParams: hashParams, updateUrl, getSearchParamsSnapshot };
}
/**
* The adapter provider component you wrap your app in.
*/
export const NuqsHashAdapter = createAdapterProvider(useHashAdapter) as FC<PropsWithChildren>;
Does not use react router, hash router, or browser router, but does routing via hashes instead of query params.
import { unstable_createAdapterProvider as createAdapterProvider } from "nuqs/adapters/custom"; import { FC, type PropsWithChildren, useEffect, useState } from "react"; export type { default } from "react";
/**
- Hook that Nuqs will call to read & write URL state. */ function useHashAdapter() { // read current state from everything after the “#” const rawHash = window.location.hash.slice(1); const [hashParams, setHashParams] = useState(new URLSearchParams(rawHash));
// write new state back into the hash fragment function updateUrl(updated: URLSearchParams) { const { pathname, search } = window.location; const hash = decodeURIComponent(updated.toString()); const url =
${pathname}${search}#${hash};window.history.pushState(null, "", url); setHashParams(updated);}
// expose a snapshot of the current params function getSearchParamsSnapshot() { const raw = window.location.hash.slice(1);
return new URLSearchParams(raw);}
// sync state when user navigates via browser controls or external script useEffect(() => { function handleHashChange() { const rawHash = window.location.hash.slice(1); setHashParams(new URLSearchParams(rawHash)); }
window.addEventListener("hashchange", handleHashChange); return () => window.removeEventListener("hashchange", handleHashChange);}, []);
return { searchParams: hashParams, updateUrl, getSearchParamsSnapshot }; }
/**
- The adapter provider component you wrap your app in. */ export const NuqsHashAdapter = createAdapterProvider(useHashAdapter) as FC<PropsWithChildren>;
@millsp - do you think you can create a PR for hash adapter?
This would be better suited as a community adapter (copy-pasted from the docs), until some of the larger refactors land (eg: #900) and we can address one of the shortcomings of combining adapters: it would make sense to have some "private" states be reflected in the hash while some "public" ones can live in the search params to be accessed server-side.
Nesting adapters should work for simple cases (as they are based on React Context), but might conflict if using rate-limiting features (debounce & throttle) as the update queue is shared and uses the URL update method of the first hook that started queuing updates. But then again those rate-limit options don't make a ton of sense for hash updates (they're mostly for controlling server-aware updates).