import locationHook from "./use-location.js";
import makeMatcher from "./matcher.js";
import {
useRef,
useLayoutEffect,
useContext,
useCallback,
createContext,
isValidElement,
cloneElement,
createElement as h,
Fragment,
} from "./react-deps.js";
/*
* Part 1, Hooks API: useRouter, useRoute and useLocation
*/
// one of the coolest features of `createContext`:
// when no value is provided — default object is used.
// allows us to use the router context as a global ref to store
// the implicitly created router (see `useRouter` below)
const RouterCtx = createContext({});
const buildRouter = ({
hook = locationHook,
base = "",
matcher = makeMatcher(),
} = {}) => ({ hook, base, matcher });
export const useRouter = () => {
const globalRef = useContext(RouterCtx);
// either obtain the router from the outer context (provided by the
// ` component) or create an implicit one on demand.
return globalRef.v || (globalRef.v = buildRouter());
};
export const useLocation = () => {
const router = useRouter();
return router.hook(router);
};
export const useRoute = (pattern) => {
const [path] = useLocation();
return useRouter().matcher(pattern, path);
};
// internal hook used by Link and Redirect in order to perform navigation
const useNavigate = (options) => {
const navRef = useRef();
const [, navigate] = useLocation();
navRef.current = () => navigate(options.to || options.href, options);
return navRef;
};
/*
* Part 2, Low Carb Router API: Router, Route, Link, Switch
*/
export const Router = (props) => {
const ref = useRef();
// this little trick allows to avoid having unnecessary
// calls to potentially expensive `buildRouter` method.
// https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily
const value = ref.current || (ref.current = { v: buildRouter(props) });
return h(RouterCtx.Provider, {
value,
children: props.children,
});
};
export const Route = ({ path, match, component, children }) => {
const useRouteMatch = useRoute(path);
// `props.match` is present - Route is controlled by the Switch
const [matches, params] = match || useRouteMatch;
if (!matches) return null;
// React-Router style `component` prop
if (component) return h(component, { params });
// support render prop or plain children
return typeof children === "function" ? children(params) : children;
};
export const Link = (props) => {
const navRef = useNavigate(props);
const { base } = useRouter();
let { to, href = to, children, onClick } = props;
const handleClick = useCallback(
(event) => {
// ignores the navigation when clicked using right mouse button or
// by holding a special modifier key: ctrl, command, win, alt, shift
if (
event.ctrlKey ||
event.metaKey ||
event.altKey ||
event.shiftKey ||
event.button !== 0
)
return;
event.preventDefault();
navRef.current();
onClick && onClick(event);
},
// navRef is a ref so it never changes
// eslint-disable-next-line react-hooks/exhaustive-deps
[onClick]
);
// wraps children in `a` if needed
const extraProps = {
// handle nested routers and absolute paths
href: href[0] === "~" ? href.slice(1) : base + href,
onClick: handleClick,
to: null,
};
const jsx = isValidElement(children) ? children : h("a", props);
return cloneElement(jsx, extraProps);
};
const flattenChildren = (children) => {
return Array.isArray(children)
? [].concat(
...children.map((c) =>
c && c.type === Fragment
? flattenChildren(c.props.children)
: flattenChildren(c)
)
)
: [children];
};
export const Switch = ({ children, location }) => {
const { matcher } = useRouter();
const [originalLocation] = useLocation();
for (const element of flattenChildren(children)) {
let match = 0;
if (
isValidElement(element) &&
// we don't require an element to be of type Route,
// but we do require it to contain a truthy `path` prop.
// this allows to use different components that wrap Route
// inside of a switch, for example .
(match = element.props.path
? matcher(element.props.path, location || originalLocation)
: [true, {}])[0]
)
return cloneElement(element, { match });
}
return null;
};
export const Redirect = (props) => {
const navRef = useNavigate(props);
// empty array means running the effect once, navRef is a ref so it never changes
useLayoutEffect(() => {
navRef.current();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return null;
};
export default useRoute;