Skip to main content
jg-packages

Search

    React Router

    jg-react-router

    React Router helpers: a page outlet that focuses each new page and restores scroll on Back and Forward, and dialogs that close when the route changes.

    v0.2.0BrowserESMMIT

    npm install jg-react-router

    Usage

    A page outlet

    PageOutlet renders the matched child route in a frame. Each new page gets focus, without scrolling, so assistive technology starts reading at it, and the window scrolls to the top. Back and Forward restore where the page was scrolled instead, retrying while its content loads, until the user scrolls or types. A navigation that changes only the query, such as a filter, keeps the page and its scroll.

    tsx
    import { createBrowserRouter } from 'react-router';
    import { PageOutlet } from 'jg-react-router';
    import 'jg-react-router/styles.css';
    
    export const router = createBrowserRouter([
      {
        path: '/',
        element: <PageOutlet className="app-main" />,
        children: [
          { index: true, element: <HomePage /> },
          { path: 'settings', element: <SettingsPage /> },
        ],
      },
    ]);

    The stylesheet gives the frame its full width and a short fade, which reduced motion turns off. It sits in the base cascade layer, so the app’s own styles and Tailwind CSS utilities override it. A layout whose nested routes should share one frame, such as a settings page with tabs, passes its own transitionKey.

    Closing dialogs when the route changes

    A dialog left open across a navigation covers the new page. DialogNavigationProvider closes the dialogs that are open when a navigation commits, before the new page’s effects run:

    tsx
    import { RouterProvider } from 'react-router';
    import { DialogNavigationProvider } from 'jg-react-router';
    
    export const App = () => (
      <DialogNavigationProvider>
        <RouterProvider router={router} />
        <ConfirmDialogHost />
      </DialogNavigationProvider>
    );

    The router’s root route renders <DialogNavigationObserver />, and its error element <DialogNavigationObserver isErrorScreen />, so a route error closes them too. Each dialog host calls useDialogNavigationDismissal with a reader for its open session, an object that stays the same while the dialog is open, and a function that closes it. Hosts that share a session close it once, and without a provider the hook does nothing.

    Page routes with their metadata

    Keep each page’s path, title, and description in one route map, and let createPageRouteObject build the router’s routes from it. Each page component receives exactly { title, description } as its props, and usePageRouteMetadata reads the deepest matched page’s, such as for the document’s title:

    tsx
    import { createBrowserRouter } from 'react-router';
    import {
      createLazyPageComponent,
      createPageRouteObject,
      type IPageRouteDefinition,
      PageOutlet,
    } from 'jg-react-router';
    
    const PAGES: IPageRouteDefinition<{ navigationLabel: string }>[] = [
      {
        path: '/',
        title: 'Home',
        description: 'Your dashboard.',
        navigationLabel: 'Home',
        Component: HomePage,
      },
      {
        path: '/settings',
        title: 'Settings',
        description: 'Your account and notifications.',
        navigationLabel: 'Settings',
        lazy: createLazyPageComponent(() => import('./settings.page.js')), // loads when visited
      },
    ];
    
    export const router = createBrowserRouter([
      { element: <PageOutlet />, children: PAGES.map((page) => createPageRouteObject(page)) },
    ]);

    An eager page becomes { path, handle, element }, and a lazy one { path, handle, HydrateFallback, lazy }, whose lazy resolves to the element. The wrapElement option wraps every page’s element, such as in an error boundary, and HydrateFallback shows while a lazy page loads during hydration. usePageRouteMetadata skips a handle without a string title and description, and gives null when no matched route is a page.

    Generated from TSDoc

    API reference

    Every export of [email protected], 21 in all, with its signature, its errors, and an example.

    Functions

    createLazyPageComponent

    function

    Turns a dynamic import of a module whose default export is a page component into a page route's lazy function, so the page's code loads only when its route is visited.

    Signature
    createLazyPageComponent(
      loadModule: () => Promise<{ default: ComponentType<IPageRouteMetadata> }>,
    ): IPageRouteLazyFunction

    Parameters

    loadModule() => Promise<{ default: ComponentType<IPageRouteMetadata> }>

    Imports the page's module.

    Returns

    IPageRouteLazyFunction

    The lazy function.

    Example
    import { createLazyPageComponent } from 'jg-react-router';
    
    declare const importSettings: Parameters<typeof createLazyPageComponent>[0];
    
    const lazy = createLazyPageComponent(importSettings); // such as () => import('./settings.page.js')

    createPageRouteElement

    function

    Renders a page component with its title and description as its props.

    Signature
    createPageRouteElement(
      PageComponent: ComponentType<IPageRouteMetadata>,
      pageMetadata: IPageRouteMetadata,
    ): ReactElement

    Parameters

    PageComponentComponentType<IPageRouteMetadata>

    The component.

    pageMetadataIPageRouteMetadata

    The title and description.

    Returns

    ReactElement

    The element.

    Example
    import { createPageRouteElement, type IPageRouteComponentProps } from 'jg-react-router';
    
    declare const SettingsPage: (props: IPageRouteComponentProps) => null;
    
    const element = createPageRouteElement(SettingsPage, { title: 'Settings', description: 'Your account.' });

    createPageRouteHandle

    function

    Creates the handle a page route carries, which usePageRouteMetadata reads.

    Signature
    createPageRouteHandle(pageMetadata: IPageRouteMetadata): IPageRouteHandle

    Parameters

    pageMetadataIPageRouteMetadata

    The page's title and description.

    Returns

    IPageRouteHandle

    The handle.

    Example
    import { createPageRouteHandle } from 'jg-react-router';
    
    const handle = createPageRouteHandle({ title: 'Settings', description: 'Your account.' });

    createPageRouteMetadata

    function

    Picks a page's title and description from its route definition, leaving out every other field.

    Signature
    createPageRouteMetadata(pageRoute: IPageRouteMetadata): IPageRouteMetadata

    Parameters

    pageRouteIPageRouteMetadata

    The definition, or anything with a title and a description.

    Returns

    IPageRouteMetadata

    The metadata.

    Example
    import { createPageRouteMetadata } from 'jg-react-router';
    
    const settings = { path: '/settings', title: 'Settings', description: 'Your account.' };
    createPageRouteMetadata(settings); // { title: 'Settings', description: 'Your account.' }

    createPageRouteObject

    function

    Turns a page route's definition into a React Router route object: the page's element, with its title and description as its props, under a handle that usePageRouteMetadata reads. An eager route renders its component; a lazy one loads it when visited, with HydrateFallback shown while it loads during hydration. wrapElement wraps the element either way.

    Signature
    createPageRouteObject<TExtraFields extends object>(
      pageRoute: IPageRouteDefinition<TExtraFields>,
      options?: ICreatePageRouteObjectOptions,
    ): RouteObject

    Parameters

    pageRouteIPageRouteDefinition<TExtraFields>

    The definition.

    options?ICreatePageRouteObjectOptions

    The fallback for lazy routes, and the wrapper for every element.

    Defaults to {}.

    Returns

    RouteObject

    The route object.

    Example
    import { createBrowserRouter } from 'react-router';
    import { createLazyPageComponent, createPageRouteObject } from 'jg-react-router';
    
    declare const importSettings: Parameters<typeof createLazyPageComponent>[0];
    
    export const router = createBrowserRouter([
      createPageRouteObject({
        path: '/settings',
        title: 'Settings',
        description: 'Your account and notifications.',
        lazy: createLazyPageComponent(importSettings),
      }),
    ]);

    DialogNavigationObserver

    function

    Reports each committed navigation to the surrounding DialogNavigationProvider, in a layout effect, so open dialogs close before the new page's effects open its own. Render it once in the router's root route, and with isErrorScreen in its error element, so a route error closes the dialogs too. Outside a provider, it renders nothing and needs no router.

    Signature
    DialogNavigationObserver(
      props: IDialogNavigationObserverProps,
    ): ReactElement<unknown, string | JSXElementConstructor<any>> | null

    Parameters

    propsIDialogNavigationObserverProps

    Whether it belongs to the root error screen.

    Returns

    ReactElement<unknown, string | JSXElementConstructor<any>> | null

    The observer, or null outside a provider.

    Example
    import { createElement } from 'react';
    import { createBrowserRouter, Outlet } from 'react-router';
    import { DialogNavigationObserver } from 'jg-react-router';
    
    export const router = createBrowserRouter([
      {
        path: '/',
        element: createElement('div', null, createElement(DialogNavigationObserver), createElement(Outlet)),
        errorElement: createElement(DialogNavigationObserver, { isErrorScreen: true }),
      },
    ]);

    DialogNavigationProvider

    function

    Closes the dialogs that are open when a navigation commits, without touching history: each dialog host registers with useDialogNavigationDismissal, and a DialogNavigationObserver inside the router reports each committed location. It reads every open session before closing any, so a dialog that closes or replaces another can't make the provider skip or close the wrong one, and hosts that share a session close it once. Without a provider, the hook and the observer do nothing.

    Signature
    DialogNavigationProvider(props: IDialogNavigationProviderProps): ReactElement

    Parameters

    propsIDialogNavigationProviderProps

    The router and the dialog hosts.

    Returns

    ReactElement

    The provider.

    Example
    import { createElement } from 'react';
    import { RouterProvider, type createBrowserRouter } from 'react-router';
    import { DialogNavigationProvider } from 'jg-react-router';
    
    declare const router: ReturnType<typeof createBrowserRouter>;
    declare const DialogHost: () => null;
    
    // the router's root route renders <DialogNavigationObserver />
    export const App = () =>
      createElement(
        DialogNavigationProvider,
        null,
        createElement(RouterProvider, { router }),
        createElement(DialogHost),
      );

    PageOutlet

    function

    Renders the matched child route in a frame that orients each new page: it focuses the frame, without scrolling, and scrolls the window to the top. Back and Forward restore the position the page had instead, retrying while its content loads and grows, until the user scrolls, touches, clicks, or types. A navigation that changes only the query keeps the page and its scroll. While an outlet is mounted, history.scrollRestoration is manual. Import jg-react-router/styles.css for the frame's layout and its fade, which reduced motion turns off.

    Signature
    PageOutlet(props: IPageOutletProps): ReactElement

    Parameters

    propsIPageOutletProps

    The frame's extra classes, and the key that identifies the page.

    Returns

    ReactElement

    The frame with the child route.

    Example
    import { createElement } from 'react';
    import { createBrowserRouter } from 'react-router';
    import { PageOutlet } from 'jg-react-router';
    
    declare const HomePage: () => null;
    declare const SettingsPage: () => null;
    
    export const router = createBrowserRouter([
      {
        path: '/',
        element: createElement(PageOutlet, { className: 'app-main' }),
        children: [
          { index: true, element: createElement(HomePage) },
          { path: 'settings', element: createElement(SettingsPage) },
        ],
      },
    ]);

    useDialogNavigationDismissal

    function

    Closes a dialog when a navigation commits while its session is open, through the surrounding DialogNavigationProvider. It always calls the latest getSession and onDismiss the component committed, so they needn't be stable, and it closes the session only if getSession still returns the one that was open when the navigation committed. Outside a provider, it does nothing.

    Signature
    useDialogNavigationDismissal(options: IDialogNavigationDismissalOptions): void

    Parameters

    optionsIDialogNavigationDismissalOptions

    How to read the dialog's session, and how to close it.

    Example
    import { useState } from 'react';
    import { useDialogNavigationDismissal } from 'jg-react-router';
    
    export const useConfirmDialog = () => {
      const [session, setSession] = useState<object | null>(null);
      useDialogNavigationDismissal({
        getSession: () => session,
        onDismiss: () => {
          setSession(null);
        },
      });
      return { isOpen: session !== null, open: () => setSession({}), close: () => setSession(null) };
    };

    usePageRouteMetadata

    function

    Gives the title and description of the deepest matched page route, such as for the document's title or a page header in a layout. A handle without a string title and description is skipped.

    Signature
    usePageRouteMetadata(): IPageRouteMetadata | null

    Returns

    IPageRouteMetadata | null

    The page's metadata, or null when no matched route is a page route.

    Example
    import { useEffect } from 'react';
    import { usePageRouteMetadata } from 'jg-react-router';
    
    export const useDocumentTitle = () => {
      const pageMetadata = usePageRouteMetadata();
      useEffect(() => {
        document.title = pageMetadata === null ? 'App' : `${pageMetadata.title} | App`;
      }, [pageMetadata]);
    };

    Types

    ICreatePageRouteObjectOptions

    type

    what createPageRouteObject takes besides the definition

    Declaration
    type ICreatePageRouteObjectOptions = {
        HydrateFallback?: RouteObject['HydrateFallback'] | undefined;
        wrapElement?: ((element: ReactElement, pageMetadata: IPageRouteMetadata) => ReactElement) | undefined;
    };

    Members

    • HydrateFallbackproperty
      RouteObject["HydrateFallback"]

      shown while a lazy page loads during hydration

    • wrapElementproperty
      (element: ReactElement, pageMetadata: IPageRouteMetadata) => ReactElement

      wraps every page's element, such as in a layout or an error boundary

    IDialogNavigationDismissalOptions

    type

    what useDialogNavigationDismissal takes: how to read the dialog's session, and how to close it, which must happen synchronously

    Declaration
    type IDialogNavigationDismissalOptions = {
        getSession: IDialogNavigationSessionReader;
        onDismiss: () => void;
    };

    Members

    • getSessionproperty
      IDialogNavigationSessionReader
    • onDismissproperty
      () => void

    IDialogNavigationObserverProps

    type

    what DialogNavigationObserver takes

    Declaration
    type IDialogNavigationObserverProps = {
        isErrorScreen?: boolean | undefined;
    };

    Members

    • isErrorScreenproperty
      boolean

      whether the observer belongs to the root error screen, which replaces the route without changing its location key

    IDialogNavigationProviderProps

    type

    what DialogNavigationProvider takes: the router and the dialog hosts that share its navigation

    Declaration
    type IDialogNavigationProviderProps = {
        children?: ReactNode;
    };

    Members

    • childrenproperty
      ReactNode

    IDialogNavigationSessionReader

    type

    reads a dialog's open session: a stable object for as long as that session lasts, or null while the dialog is closed

    Declaration
    type IDialogNavigationSessionReader = () => object | null;

    IPageOutletProps

    type

    what PageOutlet takes

    Declaration
    type IPageOutletProps = {
        className?: string | undefined;
        transitionKey?: string | undefined;
    };

    Members

    • classNameproperty
      string

      classes added to the frame after jg-page-outlet, such as the app's layout classes

    • transitionKeyproperty
      string

      what identifies the page: a new value remounts the frame, focuses it, and scrolls to the top. Defaults to the location's pathname, so a navigation that changes only the query keeps the page. A parent route's own key keeps its frame while its nested routes change

    IPageRouteComponentProps

    type

    the props a page component receives: exactly its route's title and description

    Declaration
    type IPageRouteComponentProps = IPageRouteMetadata;

    IPageRouteDefinition

    type

    A page route's definition: its path, title, and description, any fields an app adds, such as a navigation label, and either its component or a function that loads it.

    Declaration
    type IPageRouteDefinition<TExtraFields extends object = object> = IPageRouteMetadata & TExtraFields & {
        path: string;
    } & ({
        Component: ComponentType<IPageRouteComponentProps>;
        lazy?: never;
    } | {
        Component?: never;
        lazy: IPageRouteLazyFunction;
    });

    IPageRouteHandle

    type

    the handle a page route carries, which usePageRouteMetadata reads from the matches

    Declaration
    type IPageRouteHandle = {
        pageMetadata: IPageRouteMetadata;
    };

    Members

    • pageMetadataproperty
      IPageRouteMetadata

    IPageRouteLazyFunction

    type

    loads a page component on demand, such as from a dynamic import

    Declaration
    type IPageRouteLazyFunction = () => Promise<{
        Component: ComponentType<IPageRouteComponentProps>;
    }>;

    IPageRouteMetadata

    type

    a page's title and description, kept with its route

    Declaration
    type IPageRouteMetadata = {
        title: string;
        description: string;
    };

    Members

    • titleproperty
      string
    • descriptionproperty
      string