import { BehaviorSubject, merge, Observable } from 'rxjs'; import React, { JSXElementConstructor, useContext, useReducer, useEffect, useCallback, useMemo, useState } from 'react'; import { filter, map, pairwise, share, switchMap, tap } from 'rxjs/operators'; import { Location } from 'history'; import d from 'debug'; import { HistoryEvent, RouterContext } from './Router'; import { useConstant } from './useConstant'; import { processRoute } from './processRoute'; import { Ctx, PageModule, PageResponse } from './index'; const debug = d('react-sapper:PageRouter'); export interface ResolvedPage { mod: PageModule; resolved: ResolvedPath; } export interface ResolvedPath { pathname: string; pageName: string; chunkName: string; id?: number; } enum RouterStates { Resolving = 'Resolving', Resolved = 'Resolved', Complete = 'Complete', } type RouterState = { value: RouterStates; resolved: ResolvedPath | null; Cmp: JSXElementConstructor | null; }; type RouterEvents = | { type: 'resolve'; } | { type: 'resolved'; resolved: ResolvedPath; Cmp: JSXElementConstructor; } | { type: 'complete'; }; function routerReducer(state: RouterState, action: RouterEvents): RouterState { debug('action ->', action); switch (action.type) { case 'resolve': return { ...state, value: RouterStates.Resolving }; case 'resolved': return { ...state, value: RouterStates.Resolved, resolved: action.resolved, Cmp: action.Cmp, }; case 'complete': return { ...state, value: RouterStates.Complete }; } } const PageRouterContext = React.createContext<{ complete(): void; page$: BehaviorSubject<{ resolved: ResolvedPath }> }>( { page$: new BehaviorSubject<{ resolved: ResolvedPath }>({ resolved: { pathname: '/', id: -1, pageName: '', chunkName: 'none', }, }), complete: () => { console.error('PageRouterContext.complete missing, did you forget to wrap?'); }, }, ); export const PageRouterOutlet: React.FC = React.memo(() => { const { resolved, Cmp, appContext, pages, store } = useContext(Ctx); const { historyState$, replace } = useContext(RouterContext); const page$ = useConstant( () => new BehaviorSubject<{ resolved: ResolvedPath }>({ resolved }), ); const [state, dispatch] = useReducer(routerReducer, { value: RouterStates.Complete, resolved: { ...resolved }, Cmp, }); useEffect(() => { const historyPairs$: Observable<[HistoryEvent, HistoryEvent]> = historyState$.pipe(pairwise(), share()); const resolve$ = historyPairs$.pipe( filter(([prev, next]) => { // todo: not sure if this is robust enough if (prev.location.pathname === next.location.pathname) { if (prev.location.search !== next.location.search) { return false; } if (prev.location.hash !== next.location.hash) { return false; } } return ( prev.location.pathname !== next.location.pathname || prev.location.search !== next.location.search || prev.location.hash !== next.location.hash || prev.location.state !== next.location.state ); }), tap(() => dispatch({ type: 'resolve' })), map(([, next]) => next.location), switchMap((location: Location) => processRoute(location.pathname, location, store, pages, appContext)), ); const pages$ = resolve$.pipe( tap(({ response, resolved, mod }: { response: PageResponse; resolved: ResolvedPath; mod: PageModule }) => { switch (response.kind) { case 'success': case 'not-found': { dispatch({ type: 'resolved', resolved, Cmp: mod.default }); page$.next({ resolved: { ...resolved } }); break; } case 'redirect': { replace(response.location); break; } case 'error': { console.error(`Error from Page Router: ${response.message}`); } } }), ); const sub = merge(pages$).subscribe(); return () => { sub.unsubscribe(); }; }, [page$, pages, appContext, historyState$, store, replace]); const complete = useCallback(() => { dispatch({ type: 'complete' }); }, []); const api = useMemo(() => { return { complete, page$ }; }, [complete, page$]); return (
{state.Cmp && }
); }); export function usePageComplete() { const { complete } = useContext(PageRouterContext); useEffect(() => { complete(); }, [complete]); } export function usePage() { const { page$ } = useContext(PageRouterContext); const [page, setPage] = useState(page$.value); useEffect(() => { const sub = page$.subscribe((x) => setPage(x)); return () => { sub.unsubscribe(); }; }, [page$]); return page; }