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