// Inspired by react-router and universal-router import { useState, useEffect, useLayoutEffect, createElement } from 'rax'; import pathToRegexp from 'path-to-regexp'; const cache = {}; function decodeParam(val) { try { return decodeURIComponent(val); } catch (err) { return val; } } function matchPath(route, pathname, parentParams) { let { path, routes, exact: end = true, strict = false, sensitive = false } = route; // If not has path or has routes that should do not exact match if (path == null || routes) { end = false; } // Default path is empty path = path || ''; const regexpCacheKey = `${path}|${end}|${strict}|${sensitive}`; const keysCacheKey = `${regexpCacheKey }|`; let regexp = cache[regexpCacheKey]; const keys = cache[keysCacheKey] || []; if (!regexp) { regexp = pathToRegexp(path, keys, { end, strict, sensitive, }); cache[regexpCacheKey] = regexp; cache[keysCacheKey] = keys; } const result = regexp.exec(pathname); if (!result) { return null; } const url = result[0]; const params = { ...parentParams, history: router.history, location: router.history.location }; for (let i = 1; i < result.length; i++) { const key = keys[i - 1]; const prop = key.name; const value = result[i]; if (value !== undefined || !Object.prototype.hasOwnProperty.call(params, prop)) { if (key.repeat) { params[prop] = value ? value.split(key.delimiter).map(decodeParam) : []; } else { params[prop] = value ? decodeParam(value) : value; } } } return { path: !end && url.charAt(url.length - 1) === '/' ? url.slice(1) : url, params, }; } function matchRoute(route, baseUrl, pathname, parentParams) { let matched; let childMatches; let childIndex = 0; return { next() { if (!matched) { matched = matchPath(route, pathname, parentParams); if (matched) { return { done: false, $: { route, baseUrl, path: matched.path, params: matched.params, }, }; } } if (matched && route.routes) { while (childIndex < route.routes.length) { if (!childMatches) { const childRoute = route.routes[childIndex]; childRoute.parent = route; childMatches = matchRoute( childRoute, baseUrl + matched.path, pathname.slice(matched.path.length), matched.params, ); } const childMatch = childMatches.next(); if (!childMatch.done) { return { done: false, $: childMatch.$, }; } childMatches = null; childIndex++; } } return { done: true }; }, }; } let _initialized = false; let _routerConfig = null; const router = { history: null, handles: [], errorHandler() { }, addHandle(handle) { return router.handles.push(handle); }, removeHandle(handleId) { router.handles[handleId - 1] = null; }, triggerHandles(component) { router.handles.forEach((handle) => { handle && handle(component); }); }, match(fullpath) { if (fullpath == null) return; router.fullpath = fullpath; const parent = router.root; const matched = matchRoute( parent, parent.path, fullpath, ); function next(parent) { const current = matched.next(); if (current.done) { const error = new Error(`No match for ${fullpath}`); return router.errorHandler(error, router.history.location); } let { component } = current.$.route; if (typeof component === 'function') { component = component(current.$.params, router.history.location); } if (component instanceof Promise) { // Lazy loading component by import('./Foo') return component.then((component) => { // Check current fullpath avoid router has changed before lazy loading complete if (fullpath === router.fullpath) { router.triggerHandles(component); } }); } else if (component != null) { router.triggerHandles(component); return component; } else { return next(parent); } } return next(parent); }, }; function matchLocation({ pathname }) { router.match(pathname); } function getInitialComponent(routerConfig) { let InitialComponent = []; if (_routerConfig === null) { if (process.env.NODE_ENV !== 'production') { if (!routerConfig) { throw new Error('Error: useRouter should have routerConfig, see: https://www.npmjs.com/package/rax-use-router.'); } if (!routerConfig.history || !routerConfig.routes) { throw new Error('Error: routerConfig should contain history and routes, see: https://www.npmjs.com/package/rax-use-router.'); } } _routerConfig = routerConfig; } if (_routerConfig.InitialComponent) { InitialComponent = _routerConfig.InitialComponent; } router.history = _routerConfig.history; return InitialComponent; } let unlisten = null; let handleId = null; let pathes = ''; export function useRouter(routerConfig) { const [component, setComponent] = useState(getInitialComponent(routerConfig)); let newPathes = ''; if (routerConfig) { _routerConfig = routerConfig; const { routes } = _routerConfig; router.root = Array.isArray(routes) ? { routes } : routes; if (Array.isArray(routes)) { newPathes = routes.map(it => it.path).join(','); } else { newPathes = routes.path; } } if (_initialized && _routerConfig.history) { if (newPathes !== pathes) { matchLocation(_routerConfig.history.location); pathes = newPathes; } } useLayoutEffect(() => { if (unlisten) { unlisten(); unlisten = null; } if (handleId) { router.removeHandle(handleId); handleId = null; } const { history } = _routerConfig; const { routes } = _routerConfig; router.root = Array.isArray(routes) ? { routes } : routes; handleId = router.addHandle((component) => { setComponent(component); }); // Init path match if (_initialized || !_routerConfig.InitialComponent) { matchLocation(history.location); pathes = newPathes; } unlisten = history.listen(({ location }) => { matchLocation(location); pathes = newPathes; }); _initialized = true; return () => { pathes = ''; router.removeHandle(handleId); handleId = null; unlisten(); unlisten = null; }; }, []); return { component }; } export function withRouter(Component) { function Wrapper(props) { const { history } = router; return createElement(Component, { ...props, history, location: history.location }); } Wrapper.displayName = `withRouter(${ Component.displayName || Component.name })`; Wrapper.WrappedComponent = Component; return Wrapper; }