54 lines
1.6 KiB
JavaScript
54 lines
1.6 KiB
JavaScript
import { useSyncExternalStore } from './react-deps.js';
|
|
|
|
// array of callback subscribed to hash updates
|
|
const listeners = {
|
|
v: [],
|
|
};
|
|
|
|
const onHashChange = () => listeners.v.forEach((cb) => cb());
|
|
|
|
// we subscribe to `hashchange` only once when needed to guarantee that
|
|
// all listeners are called synchronously
|
|
const subscribeToHashUpdates = (callback) => {
|
|
if (listeners.v.push(callback) === 1)
|
|
addEventListener("hashchange", onHashChange);
|
|
|
|
return () => {
|
|
listeners.v = listeners.v.filter((i) => i !== callback);
|
|
if (!listeners.v.length) removeEventListener("hashchange", onHashChange);
|
|
};
|
|
};
|
|
|
|
// leading '#' is ignored, leading '/' is optional
|
|
const currentHashLocation = () => "/" + location.hash.replace(/^#?\/?/, "");
|
|
|
|
const navigate = (to, { state = null } = {}) => {
|
|
// calling `replaceState` allows us to set the history
|
|
// state without creating an extra entry
|
|
const [hash, search] = to.replace(/^#?\/?/, "").split("?");
|
|
|
|
history.replaceState(
|
|
state,
|
|
"",
|
|
// keep the current pathname, but replace query string and hash
|
|
location.pathname +
|
|
(search ? `?${search}` : location.search) +
|
|
// update location hash, this will cause `hashchange` event to fire
|
|
// normalise the value before updating, so it's always preceeded with "#/"
|
|
(location.hash = `#/${hash}`)
|
|
);
|
|
};
|
|
|
|
const useHashLocation = ({ ssrPath = "/" } = {}) => [
|
|
useSyncExternalStore(
|
|
subscribeToHashUpdates,
|
|
currentHashLocation,
|
|
() => ssrPath
|
|
),
|
|
navigate,
|
|
];
|
|
|
|
useHashLocation.hrefs = (href) => "#" + href;
|
|
|
|
export { navigate, useHashLocation };
|