1import { useCallback, useMemo, useState, useRef, useEffect } from 'react';
2import { last, includes as _includes, findIndex, get } from 'lodash';
3import { ROUTES } from './router';
4import { ROUTE_NAMES } from './routeNames';
8type RouteKey = keyof typeof ROUTES | string;
9export interface Route {
14interface UseRouterParams {
15 names: Record<string, string>;
17 initialRoutes: Route[];
20type OnPopStateHandler = (ev: PopStateEvent) => any;
22export interface Router {
24 names: typeof ROUTE_NAMES | Record<string, string>;
26 push: (key: RouteKey, data?: any) => void;
27 popAndPush: (key: RouteKey, data?: any) => void;
29 popTo: (key: string) => void;
30 replaceWith: (routes: Route[]) => void;
32 peek: () => Route | undefined;
34 includes: (key: RouteKey) => boolean;
39 * @param names map of string to view key, used for consumer references
40 * @param views map of view keys to react component views
41 * @param initialRoutes initial set of routes following the { key, data } format
43export default function useRouter({
44 names = {} as typeof ROUTE_NAMES,
47}: UseRouterParams): Router {
48 const [routes, setRoutes] = useState(initialRoutes);
50 const oldPopState = useRef<OnPopStateHandler>();
52 const size = routes.length;
53 const push = useCallback(
57 `Invalid Route: ${key}. Possible Routes: ${Object.keys(views).join(
62 setRoutes(routes => [...routes, { key, data }]);
66 const popAndPush = useCallback(
70 `Invalid Route: ${key}. Possible Routes: ${Object.keys(views).join(
77 ...routes.slice(0, routes.length - 1),
83 const pop = useCallback(
86 // if we are at the root, pass this event to our parent
87 if (oldPopState.current) {
88 oldPopState.current({} as PopStateEvent);
93 // on pop, tell the browser of a new state to avoid giving the user
94 // the ability to go forward
95 // TODO: allow the user to go forward by storing our data in history
96 // and using the url for other state
97 // (or just importing an actually good router lib)
98 window.history.pushState(null, '', null);
101 return setRoutes(routes => [...routes.slice(0, routes.length - count)]);
103 [size, setRoutes, oldPopState]
105 const popTo = useCallback(
107 const index = findIndex(routes, route => route.key === name);
108 return pop(routes.length - 1 - index);
112 const peek = useCallback(() => last(routes), [routes]);
113 const replaceWith = useCallback(
114 (routes: Route[]) => setRoutes(() => routes),
117 const reset = useCallback(() => setRoutes(initialRoutes), [
121 const includes = useCallback(
124 routes.map(r => r.key),
129 const data = useMemo(() => {
130 return get(last(routes), 'data', NULL_DATA);
132 const Route = useMemo(() => {
133 const key = peek()?.key;
142 // Scroll to top of page with each route transition
144 window.scrollTo(0, 0);
148 // on router mount, register new state with browser
149 window.history.pushState(null, '', null);
152 // capture browser pop in primary router
154 // store the previous onpopstate handler
155 oldPopState.current = window.onpopstate?.bind(window) as OnPopStateHandler;
157 // construct new onpopstate handler
158 window.onpopstate = e => {
159 e && e.stopImmediatePropagation();
164 // reset the onpopstate to the previous version
165 window.onpopstate = oldPopState.current as typeof window.onpopstate;
167 }, [size, pop, oldPopState]);
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list