on f660f7b help sign in
bridge/src/lib/useRouter.js
133 lines 3.5 kB history raw
1import { useCallback, useMemo, useState, useRef, useEffect } from 'react';
2import { last, includes as _includes, findIndex } from 'lodash';
4/**
5 * @param primary whether or not this is the top-level router
6 * @param names map of string to view key, used for consumer references
7 * @param views map of view keys to react component views
8 * @param initialRoutes initial set of routes following the { key, data } format
9 */
10export default function useRouter({
11 primary = false,
12 names = {},
13 views = {},
14 initialRoutes = [],
15}) {
16 const [routes, setRoutes] = useState(initialRoutes);
18 const oldPopState = useRef();
20 const size = routes.length;
21 const push = useCallback(
22 (key, data) => {
23 if (!views[key]) {
24 throw new Error(
25 `Invalid Route: ${key}. Possible Routes: ${Object.keys(views).join(
26 ', '
27 )}`
28 );
29 }
30 setRoutes(routes => [...routes, { key, data }]);
31 },
32 [setRoutes, views]
33 );
34 const popAndPush = useCallback(
35 (key, data = {}) => {
36 if (!views[key]) {
37 throw new Error(
38 `Invalid Route: ${key}. Possible Routes: ${Object.keys(views).join(
39 ', '
40 )}`
41 );
42 }
44 setRoutes(routes => [
45 ...routes.slice(0, routes.length - 1),
46 { key, data },
47 ]);
48 },
49 [setRoutes, views]
50 );
51 const pop = useCallback(
52 (count = 1) => {
53 if (size > 1) {
54 // pop as expected
55 return setRoutes(routes => [...routes.slice(0, routes.length - count)]);
56 }
58 // if we are at the root, pass this event to our parent
59 if (oldPopState.current) {
60 window.history.back();
61 }
62 },
63 [size, setRoutes, oldPopState]
64 );
65 const popTo = useCallback(
66 name => {
67 const index = findIndex(routes, route => route.key === name);
68 return pop(routes.length - 1 - index);
69 },
70 [pop, routes]
71 );
72 const peek = useCallback(() => last(routes), [routes]);
73 const replaceWith = useCallback(routes => setRoutes(() => routes), [
74 setRoutes,
75 ]);
76 const reset = useCallback(() => setRoutes(initialRoutes), [
77 setRoutes,
78 initialRoutes,
79 ]);
80 const includes = useCallback(key => _includes(routes.map(r => r.key), key), [
81 routes,
82 ]);
83 const data = useMemo(() => last(routes).data, [routes]);
84 const Route = useMemo(() => views[peek().key], [views, peek]);
86 // Scroll to top of page with each route transition
87 useEffect(() => {
88 window.scrollTo(0, 0);
89 }, [routes]);
91 // capture browser pop in primary router
92 useEffect(() => {
93 // store the previous onpopstate handler
94 oldPopState.current = window.onpopstate;
96 // construct new onpopstate handler
97 window.onpopstate = e => {
98 if (size <= 1 && oldPopState.current) {
99 // if this is the root route and there's a parent handler,
100 // give the event to the handler
101 return oldPopState.current(e);
102 }
104 // on pop, tell the browser of a new state to avoid giving the user
105 // the ability to go forward
106 window.history.pushState(null, null, null);
108 // then update our local state for rendering
109 pop();
110 };
112 return () => {
113 // reset the onpopstate to the previous version
114 window.onpopstate = oldPopState.current;
115 };
116 }, [size, pop, oldPopState]);
118 return {
119 Route,
120 names,
121 routes,
122 push,
123 popAndPush,
124 pop,
125 popTo,
126 replaceWith,
127 reset,
128 peek,
129 size,
130 includes,
131 data,
132 };