AppRouter

A component that sets up Squide's primitives with a React Router instance.

Reference

<AppRouter waitForPublicData={boolean} waitForProtectedData={boolean} strictMode={boolean}>
    {({ rootRoute, registeredRoutes, routerProps, routerProviderProps }) => ( ... )}
</AppRouter>

Properties

  • waitForPublicData: An optional boolean value indicating whether or not Squide should delay the rendering of the requested page until the public data is ready. The default value is false.
  • waitForProtectedData: An optional boolean value indicating whether or not Squide should delay the rendering of the requested page until the protected data is ready. The default value is false.
  • strictMode: An optional boolean value indicating whether or not Squide should validate the registrations once the modules are ready and throw when the active location matches no registered route. The default value is true.
  • children: A render function defining a RouterProvider component with rootRoute, registeredRoutes, routerProps and routerProviderProps.

Usage

Define a router provider

The rootRoute component provided as an argument to the AppRouter rendering function must always be rendered as a parent of the registeredRoutes.

host/src/App.tsx
import { AppRouter } from "@squide/firefly";
import { createBrowserRouter } from "react-router";
import { RouterProvider } from "react-router/dom";

export function App() {
    <AppRouter >
        {({ rootRoute, registeredRoutes, routerProps, routerProviderProps }) => {
            return (
                <RouterProvider
                    router={createBrowserRouter([
                        {
                            element: rootRoute,
                            children: registeredRoutes
                        }
                    ], routerProps)}
                    {...routerProviderProps}
                />
            );
        }}
    </AppRouter>
}

Define a loading component

A BootstrappingRoute component is introduced in the following example because the useIsBootstrapping hook must be rendered as a child of rootRoute.

host/src/App.tsx
import { useIsBootstrapping, AppRouter } from "@squide/firefly";
import { createBrowserRouter, Outlet } from "react-router";
import { RouterProvider } from "react-router/dom";

function BootstrappingRoute() {
    if (useIsBootstrapping()) {
        return <div>Loading...</div>;
    }

    return <Outlet />;
}

export function App() {
    return (
        <AppRouter>
            {({ rootRoute, registeredRoutes, routerProps, routerProviderProps }) => {
                return (
                    <RouterProvider
                        router={createBrowserRouter([
                            {
                                element: rootRoute,
                                children: [
                                    {
                                        element: <BootstrappingRoute />,
                                        children: registeredRoutes
                                    }
                                ]
                            }
                        ], routerProps)}
                        {...routerProviderProps}
                    />
                );
            }}
        </AppRouter>
    );
}

Define a root error boundary

A React Router errorElement retrieves the current error using the useRouteError hook. The root error boundary should always wrap the registeredRoutes and, when application, the BootstrapingRoute component.

host/src/RootErrorBoundary.tsx
import { isGlobalDataQueriesError, useLogger } from "@squide/firefly";
import { useRouteError, isRouteErrorResponse } from "react-router";
import { useEffect } from "react";

export function RootErrorBoundary() {
    const error = useRouteError() as Error;
    const location = useLocation();
    const logger = useLogger();

    useEffect(() => {
        if (isRouteErrorResponse(error)) {
            logger.error(`An unmanaged error occurred while rendering the route with path ${location.pathname} ${error.status} ${error.statusText}.`);
        } else if (isGlobalDataQueriesError(error)) {
            logger
                .withText(`An unmanaged error occurred while rendering the route with path ${location.pathname}:`)
                .withText(error.message)
                .withObject(error.errors)
                .error();
        } else {
            logger
                .withText(`[shell] An unmanaged error occurred while rendering the route with path ${location.pathname}:`)
                .withError(error)
                .error();
        }
    }, [location.pathname, error, logger]);

    return (
        <div>
            <h2>Unmanaged error</h2>
            <p>An unmanaged error occurred and the application is broken, try refreshing your browser.</p>
        </div>
    );
}
host/src/App.tsx
import { AppRouter } from "@squide/firefly";
import { createBrowserRouter } from "react-router";
import { RouterProvider } from "react-router/dom";
import { RootErrorBoundary } from "./RootErrorBoundary.tsx";

export function App() {
    return (
        <AppRouter>
            {({ rootRoute, registeredRoutes, routerProps, routerProviderProps }) => {
                return (
                    <RouterProvider
                        router={createBrowserRouter([
                            {
                                element: rootRoute,
                                errorElement: <RootErrorBoundary />,
                                children: registeredRoutes
                            }
                        ], routerProps)}
                        {...routerProviderProps}
                    />
                );
            }}
        </AppRouter>
    );
}

Delay rendering until the public data is ready

A BootstrappingRoute component is introduced in the following example because the usePublicDataQueries hook must be rendered as a child of rootRoute.

host/src/App.tsx
import { useIsBootstrapping, usePublicDataQueries, AppRouter } from "@squide/firefly";
import { createBrowserRouter, Outlet } from "react-router";
import { RouterProvider } from "react-router/dom";
import { FeatureFlagsContext } from "@sample/shared";
import { getFeatureFlagsQuery } from "./getFeatureFlagsQuery.ts";

function BootstrappingRoute() {
    const [featureFlags] = usePublicDataQueries([getFeatureFlagsQuery]);

    if (useIsBootstrapping()) {
        return <div>Loading...</div>;
    }

    return (
        <FeatureFlagsContext.Provider value={featureFlags}>
            <Outlet />
        </FeatureFlagsContext.Provider>
    );
}

export function App() {
    return (
        <AppRouter waitForPublicData>
            {({ rootRoute, registeredRoutes, routerProps, routerProviderProps }) => {
                return (
                    <RouterProvider
                        router={createBrowserRouter([
                            {
                                element: rootRoute,
                                children: [
                                    {
                                        element: <BootstrappingRoute />,
                                        children: registeredRoutes
                                    }
                                ]
                            }
                        ], routerProps)}
                        {...routerProviderProps}
                    />
                );
            }}
        </AppRouter>
    );
}

Delay rendering until the protected data is ready

A BootstrappingRoute component is introduced in the following example because the useProtectedDataQueries hook must be rendered as a child of rootRoute.

host/src/App.tsx
import { useIsBootstrapping, useProtectedDataQueries, AppRouter } from "@squide/firefly";
import { createBrowserRouter, Outlet } from "react-router";
import { RouterProvider } from "react-router/dom";
import { SessionContext, isApiError } from "@sample/shared";
import { getSessionQuery } from "./getSessionQuery.ts";

function BootstrappingRoute() {
    const [session] = useProtectedDataQueries(
        [getSessionQuery],
        error => isApiError(error) && error.status === 401
    );

    if (useIsBootstrapping()) {
        return <div>Loading...</div>;
    }

    return (
        <SessionContext.Provider value={session}>
            <Outlet />
        </SessionContext.Provider>
    );
}

export function App() {
    return (
        <AppRouter waitForProtectedData>
            {({ rootRoute, registeredRoutes, routerProps, routerProviderProps }) => {
                return (
                    <RouterProvider
                        router={createBrowserRouter([
                            {
                                element: rootRoute,
                                children: [
                                    {
                                        element: <BootstrappingRoute />,
                                        children: registeredRoutes
                                    }
                                ]
                            }
                        ], routerProps)}
                        {...routerProviderProps}
                    />
                );
            }}
        </AppRouter>
    );
}

Disable strict mode

By default, Squide validates the registrations once the modules are ready. A navigation item registered under a section that no module registered, or a route registered under a parent that does not exist, throws in development and is logged in production.

Strict mode additionally throws when the active location matches no registered route.

Set strictMode to false to turn all of it off:

host/src/App.tsx
import { AppRouter } from "@squide/firefly";

export function App() {
    return (
        <AppRouter strictMode={false}>
            {({ rootRoute, registeredRoutes, routerProps, routerProviderProps }) => ( ... )}
        </AppRouter>
    );
}