nuqs icon indicating copy to clipboard operation
nuqs copied to clipboard

react-router HashRouter support?

Open Jungzl opened this issue 1 year ago • 10 comments

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

  1. go to '/form'
  2. type something, press Enter
  3. see url

Jungzl avatar Dec 16 '24 03:12 Jungzl

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.

franky47 avatar Dec 16 '24 05:12 franky47

supp future self!

sebbean avatar Dec 29 '24 08:12 sebbean

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 avatar Jan 17 '25 18:01 gsaandy

@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 avatar Jan 17 '25 19:01 franky47

@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

gsaandy avatar Jan 19 '25 18:01 gsaandy

@franky47 - is this taken care in the latest version?

gsaandy avatar Feb 17 '25 09:02 gsaandy

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).

franky47 avatar Feb 17 '25 10:02 franky47

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 avatar May 08 '25 15:05 millsp

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?

gsaandy avatar Jun 07 '25 21:06 gsaandy

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).

franky47 avatar Jun 10 '25 07:06 franky47