/// import React, { useContext, useState, useEffect, Suspense, useTransition, } from 'react' import RoutingContext, { Entry } from './RoutingContext' import ErrorBoundary from './ErrorBoundary' interface RouterRendererProps { LoadingComponent: React.FC transitionConfig: React.SuspenseConfig } const RouteRenderer: React.FC = ({ LoadingComponent, transitionConfig, }) => { // Access the router const router = useContext(RoutingContext) if (router == null) { throw new Error('RoutingContext not set') } // Improve the route transition UX by delaying transitions: show the previous route entry // for a brief period while the next route is being prepared. See // https://reactjs.org/docs/concurrent-mode-patterns.html#transitions const [startTransition, isPending] = useTransition(transitionConfig) // Store the active entry in state - this allows the renderer to use features like // useTransition to delay when state changes become visible to the user. const [routeEntry, setRouteEntry] = useState(router.get()) // On mount subscribe to route changes useEffect(() => { // Check if the route has changed between the last render and commit: const currentEntry = router.get() if (currentEntry !== routeEntry) { // if there was a concurrent modification, rerender and exit setRouteEntry(currentEntry) return } // If there *wasn't* a concurrent change to the route, then the UI // is current: subscribe for subsequent route updates const dispose = router.subscribe(nextEntry => { // startTransition() delays the effect of the setRouteEntry (setState) call // for a brief period, continuing to show the old state while the new // state (route) is prepared. startTransition(() => { setRouteEntry(nextEntry) }) }) return () => dispose() // Note: this hook updates routeEntry manually; we exclude that variable // from the hook deps to avoid recomputing the effect after each change // triggered by the effect itself. // eslint-disable-next-line }, [router, startTransition]) // The current route value is an array of matching entries - one entry per // level of routes (to allow nested routes). We have to map each one to a // RouteComponent to allow suspending, and also pass its children correctly. // Conceptually, we want this structure: // ``` // // // // continue for nested items... // // // ``` // To achieve this, we reverse the list so we can start at the bottom-most // component, and iteratively construct parent components w the previous // value as the child of the next one: const reversedItems = [...routeEntry.entries].reverse() // reverse is in place, but we want a copy so concat const firstItem = reversedItems[0] // the bottom-most component is special since it will have no children // (though we could probably just pass null children to it) let routeComponent = ( ) for (let ii = 1; ii < reversedItems.length; ii++) { const nextItem = reversedItems[ii] routeComponent = ( {routeComponent} ) } // Routes can error so wrap in an // Routes can suspend, so wrap in return ( {/* Indicate to the user that a transition is pending, even while showing the previous UI */} {isPending ? : null} {routeComponent} ) } export default RouteRenderer /** * The `component` property from the route entry is a Resource, which may or may not be ready. * We use a helper child component to unwrap the resource with component.read(), and then * render it if its ready. * * NOTE: calling routeEntry.route.component.read() directly in RouteRenderer woldn't work the * way we'd expect. Because that method could throw - either suspending or on error - the error * would bubble up to the *caller* of RouteRenderer. We want the suspend/error to bubble up to * our ErrorBoundary/Suspense components, so we have to ensure that the suspend/error happens * in a child component. */ const RouteComponent: React.FC = props => { const Component = props.component!.read()! const { routeData, prepared } = props return ( ) }