87 lines
2.9 KiB
JavaScript
87 lines
2.9 KiB
JavaScript
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
|
|
// useEffect(() => {
|
|
// While History API does have `popstate` event, the only
|
|
// proper way to listen to changes via `push/replaceState`
|
|
// is to monkey-patch these methods.
|
|
//
|
|
// See https://stackoverflow.com/a/4585031
|
|
if (typeof history !== 'undefined') {
|
|
for (const type of [eventPushState, eventReplaceState]) {
|
|
const original = history[type];
|
|
|
|
history[type] = function () {
|
|
const result = original.apply(this, arguments);
|
|
const event = new Event(type);
|
|
event.arguments = arguments;
|
|
|
|
dispatchEvent(event);
|
|
return result;
|
|
};
|
|
}
|
|
} else {
|
|
console.log('No history API');
|
|
}
|
|
// });
|
|
|
|
/**
|
|
* History API docs @see https://developer.mozilla.org/en-US/docs/Web/API/History
|
|
*/
|
|
const eventPopstate = 'popstate';
|
|
const eventPushState = 'pushState';
|
|
const eventReplaceState = 'replaceState';
|
|
export const events = [eventPopstate, eventPushState, eventReplaceState];
|
|
|
|
const useLocation = ({ base = '' } = {}) => {
|
|
const getCurrentPathname = useCallback(base => {
|
|
const path = typeof location === 'undefined' ? '/' : location.pathname;
|
|
|
|
return !path.toLowerCase().indexOf(base.toLowerCase())
|
|
? path.slice(base.length) || '/'
|
|
: '~' + path;
|
|
}, []);
|
|
|
|
const [path, update] = useState(() => getCurrentPathname(base)); // @see https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
|
|
const prevPath = useRef(path);
|
|
|
|
useEffect(() => {
|
|
// this function checks if the location has been changed since the
|
|
// last render and updates the state only when needed.
|
|
// unfortunately, we can't rely on `path` value here, since it can be stale,
|
|
// that's why we store the last pathname in a ref.
|
|
const checkForUpdates = () => {
|
|
const pathname = getCurrentPathname(base);
|
|
console.log('CHECK FOR UPDATES', pathname);
|
|
prevPath.current !== pathname && update((prevPath.current = pathname));
|
|
};
|
|
|
|
events.map(e => addEventListener(e, checkForUpdates));
|
|
|
|
// it's possible that an update has occurred between render and the effect handler,
|
|
// so we run additional check on mount to catch these updates. Based on:
|
|
// https://gist.github.com/bvaughn/e25397f70e8c65b0ae0d7c90b731b189
|
|
checkForUpdates();
|
|
|
|
return () => events.map(e => removeEventListener(e, checkForUpdates));
|
|
}, [base]);
|
|
|
|
// the 2nd argument of the `useLocation` return value is a function
|
|
// that allows to perform a navigation.
|
|
//
|
|
// the function reference should stay the same between re-renders, so that
|
|
// it can be passed down as an element prop without any performance concerns.
|
|
const navigate = useCallback(
|
|
(to, { replace = false } = {}) =>
|
|
history[replace ? eventReplaceState : eventPushState](
|
|
null,
|
|
'',
|
|
// handle nested routers and absolute paths
|
|
to[0] === '~' ? to.slice(1) : base + to
|
|
),
|
|
[base]
|
|
);
|
|
|
|
return [path, navigate];
|
|
};
|
|
export default useLocation;
|