# Fetch protected global data

To make global protected data fetching easier, Squide provides primitives build on top of TanStack Query to orchestrate both the data-loading states and the associated UI.

At first glance, one might wonder what could be so complicated about fetching the global data of an application. It's only fetches ...right? Well, there are several concerns to take into account for a modular application:

  • When in development, the global data cannot be fetched until the Mock Service Worker (MSW) request handlers are registered and MSW is ready.
  • To register the MSW request handlers, the modules must be registered first.
  • If the requested page is public, only the global public data should be fetched.
  • If the requested page is protected, both the global public and protected data should be fetched.
  • The requested page rendering must be delayed until the global data has been fetched.
  • A unique loading spinner should be displayed to the user during this process, ensuring there's no flickering due to different spinners being rendered.

Before fetching data with TanStack Query, start by following the setup Tanstack Query integration guide to setup the Query client and the React provider. Once the setup is complete, the examples below cover the most common use cases.

For more details, refer to the reference documentation.

# Fetch data

👉 There are four key steps to fetch global protected data:

Here's an example:

import { AppRouter, useProtectedDataQueries, useIsBootstrapping } from "@squide/firefly";
import { createBrowserRouter, Outlet } from "react-router";
import { RouterProvider } from "react-router/dom";
import { SubscriptionContext, ApiError } from "@sample/shared";

function BootstrappingRoute() {
    const [subscription] = useProtectedDataQueries([
        {
            queryKey: ["/api/subscription"],
            queryFn: async () => {
                const response = await fetch("/api/subscription");

                if (!response.ok) {
                    throw new ApiError(response.status, response.statusText);
                }

                const data = await response.json();

                return data.status as string;
            }
        }
    ], error => isApiError(error) && error.status === 401);

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

    return (
        <SubscriptionContext.Provider value={subscription}>
            <Outlet />
        </SubscriptionContext.Provider>
    );
}

export function App() {
    return (
        <AppRouter waitForProtectedData>
            {({ rootRoute, registeredRoutes, routerProviderProps }) => {
                return (
                    <RouterProvider
                        router={createBrowserRouter([
                            {
                                element: rootRoute,
                                children: [
                                    {
                                        element: <BootstrappingRoute />,
                                        children: registeredRoutes
                                    }
                                ]
                            }
                        ])}
                        {...routerProviderProps}
                    />
                );
            }}
        </AppRouter>
    );
}
@sample/shared
import { createContext, useContext } from "react";

export interface Subscription {
    status: string
}

export const SubscriptionContext = createContext(Subscription | undefined);

export function useSubscription() {
    return useContext(SubscriptionContext);
}
@sample/shared
export class ApiError extends Error {
    readonly #status: number;
    readonly #statusText: string;
    readonly #stack?: string;

    constructor(status: number, statusText: string, innerStack?: string) {
        super(`${status} ${statusText}`);

        this.#status = status;
        this.#statusText = statusText;
        this.#stack = innerStack;
    }

    get status() {
        return this.#status;
    }

    get statusText() {
        return this.#statusText;
    }

    get stack() {
        return this.#stack;
    }
}

export function isApiError(error?: unknown): error is ApiError {
    return error !== undefined && error !== null && error instanceof ApiError;
}

First, define an MSW request handler that returns the number of times it has been fetched:

mocks/handlers.ts
import { HttpResponse, http, type HttpHandler } from "msw";

export const requestHandlers: HttpHandler[] = [
    http.get("/api/subscription", () => {
        // NOTE: The user id should be retrieved from the current session and the subscription should be retrieved from a database with this id.
        // For the sake of simplicity, we haven't done it for this guide, instead we return hardcoded data.
        return HttpResponse.json([{
            "status": "paid"
        }]);
    })
];

Then, register the request handler using the module registration function:

import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly"; 

export const register: ModuleRegisterFunction<FireflyRuntime> = async runtime => {
    if (runtime.isMswEnabled) {
        // Files that includes an import to the "msw" package are included dynamically to prevent adding
        // unused MSW code to the production bundles.
        const requestHandlers = (await import("../mocks/handlers.ts")).requestHandlers;

        runtime.registerRequestHandlers(requestHandlers);
    }
}
import { useSubscription } from "@sample/shared";

export function Page() {
    const subscription = useSubscription();

    return (
        <p>Subscription status: {subscription?.status}</p>
    )
}

# Handle fetch errors

The useProtectedDataQueries hook can throw GlobalDataQueriesError instances, which are typically unmanaged and should be handled by an error boundary. To assert in an error boundary that an error is an instance of GlobalDataQueriesError, use the isGlobalDataQueriesError function:

import { useLogger, isGlobalDataQueriesError } from "@squide/firefly";
import { useLocation, useRouteError } from "react-router/dom";

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

    useEffect(() => {
        if (isGlobalDataQueriesError(error)) {
            logger
                .withText(`[shell] An unmanaged error occurred while rendering the route with path ${location.pathname} ${error.message}`)
                .withError(error.errors)
                .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>
    );
}

# Setup TanStack Query

Fetching data with the useProtectedDataQueries hook requires TanStack Query to be configured. To set it up, follow the Setup TanStack Query integration guide.

# Register a conditionnal navigation item

To register a navigation item based on protected remote data, refer to the register deferred navigation items guide.