on d9057f7 help sign in
bridge/src/lib/useRouter.js
139 lines 3.6 kB history raw
1import { useCallback, useMemo, useState, useRef, useEffect } from 'react';
2import { last, includes as _includes, findIndex, get } from 'lodash';
4const NULL_DATA = {};
6/**
7 * @param names map of string to view key, used for consumer references
8 * @param views map of view keys to react component views
9 * @param initialRoutes initial set of routes following the { key, data } format
10 */
11export default function useRouter({
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 // if we are at the root, pass this event to our parent
55 if (oldPopState.current) {
56 oldPopState.current();
57 }
58 return;
59 }
61 // on pop, tell the browser of a new state to avoid giving the user
62 // the ability to go forward
63 // TODO: allow the user to go forward by storing our data in history
64 // and using the url for other state
65 // (or just importing an actually good router lib)
66 window.history.pushState(null, null, null);
68 // pop as expected
69 return setRoutes(routes => [...routes.slice(0, routes.length - count)]);
70 },
71 [size, setRoutes, oldPopState]
72 );
73 const popTo = useCallback(
74 name => {
75 const index = findIndex(routes, route => route.key === name);
76 return pop(routes.length - 1 - index);
77 },
78 [pop, routes]
79 );
80 const peek = useCallback(() => last(routes), [routes]);
81 const replaceWith = useCallback(routes => setRoutes(() => routes), [
82 setRoutes,
83 ]);
84 const reset = useCallback(() => setRoutes(initialRoutes), [
85 setRoutes,
86 initialRoutes,
87 ]);
88 const includes = useCallback(key => _includes(routes.map(r => r.key), key), [
89 routes,
90 ]);
91 const data = useMemo(() => {
92 return get(last(routes), 'data', {});
93 // return last(routes).data || NULL_DATA;
94 }, [routes]);
95 const Route = useMemo(() => views[peek().key], [views, peek]);
97 // Scroll to top of page with each route transition
98 useEffect(() => {
99 window.scrollTo(0, 0);
100 }, [routes]);
102 useEffect(() => {
103 // on router mount, register new state with browser
104 window.history.pushState(null, null, null);
105 }, []);
107 // capture browser pop in primary router
108 useEffect(() => {
109 // store the previous onpopstate handler
110 oldPopState.current = window.onpopstate;
112 // construct new onpopstate handler
113 window.onpopstate = e => {
114 e && e.stopImmediatePropagation();
115 pop();
116 };
118 return () => {
119 // reset the onpopstate to the previous version
120 window.onpopstate = oldPopState.current;
121 };
122 }, [size, pop, oldPopState]);
124 return {
125 Route,
126 names,
127 routes,
128 push,
129 popAndPush,
130 pop,
131 popTo,
132 replaceWith,
133 reset,
134 peek,
135 size,
136 includes,
137 data,
138 };