import { BehaviorSubject, merge, Observable, Subject } from 'rxjs'; import React, { useContext, useEffect, useCallback, useMemo } from 'react'; import { Action, History, Location, createBrowserHistory, LocationState, Path } from 'history'; import { tap } from 'rxjs/operators'; import { useConstant } from './useConstant'; import { Ctx } from './index'; interface HistoryMethodCall { method: 'push' | 'replace'; args: [Path, LocationState?]; } /** * This is the global router context/state */ export const RouterContext = React.createContext<{ historyState$: BehaviorSubject; push: (path: Path, state?: LocationState) => void; replace: (path: Path, state?: LocationState) => void; }>({ historyState$: new BehaviorSubject({ action: 'POP', location: { pathname: '', search: '', hash: '', state: {} }, } as HistoryEvent), push: (..._args: any[]) => { console.error('Not implemented - have you wrapped?'); }, replace: (..._args: any[]) => { console.error('Not implemented - have you wrapped?'); }, }); export const RouterProvider: React.FC = React.memo((props) => { /** * This is the initial location/action param given in both SSR and browser environments */ const { location, action } = useContext(Ctx); /** * This is a stream that allows child components to invoke history methods * (such as the component) */ const historyMethodInvocations$ = useConstant(() => new Subject()); /** * This tracks location/actions and can be subscribed to to receive the current value */ const historyState$ = useConstant( () => new BehaviorSubject({ location, action }), ); /** * Expose a 'push' method on the context */ const push = useCallback( (path: Path, state?: LocationState) => { historyMethodInvocations$.next({ method: 'push', args: [path, state] }); }, [historyMethodInvocations$], ); /** * Expose a 'replace' method on the context */ const replace = useCallback( (path: Path, state?: LocationState) => { historyMethodInvocations$.next({ method: 'replace', args: [path, state] }); }, [historyMethodInvocations$], ); useEffect(() => { /** * Create a browser history. Done here to prevent errors in SSR */ const history = createBrowserHistory(); /** * Apply the history invocations */ const appliedHistoryMethod$ = historyMethodInvocations$.pipe( tap((x) => history[x.method].apply(null, x.args as any)), ); /** * A listener for raw history events (such as back button etc) */ const historyEvents$ = historyAsObs(history).pipe(tap((x) => historyState$.next(x))); /** * Begin all subscriptions */ const sub = merge(appliedHistoryMethod$, historyEvents$).subscribe(); /** * Tear down */ return () => sub.unsubscribe(); }, [historyMethodInvocations$, historyState$]); /** * This is the public API of the context this component provides */ const api = useMemo(() => { return { push, replace, historyState$, }; }, [push, historyState$, replace]); return {props.children}; }); export interface HistoryEvent { location: Location; action: Action; } /** * Wrap raw events from the history obj as an observable */ export function historyAsObs(history: History): Observable { return new Observable((obs) => { const handler = (location: Location, action: Action) => { obs.next({ location, action, }); }; const unlisten = history.listen(handler); return () => { if (typeof unlisten === 'function') { // debug('history listener teardown'); unlisten(); } else { console.error('Could not tear down the history component'); } }; }); } /** * Re-exports for convenience */ export { Link } from './Link'; export { useLocation } from './useLocation';