#
useProtectedDataQueries
Execute the specified Tanstack queries when the modules are ready, the active route is protected and, when applicable, Mock Service Worker is ready.
Use this hook to fetch protected global data during the bootstrapping phase of your application. Avoid using it in product feature components.
#
Reference
const results = useProtectedDataQueries(queries: [], isUnauthorizedError: (error) => boolean)
#
Parameters
queries
: An array of QueriesOptions.isUnauthorizedError
: A function that returns aboolean
value indicating whether or not the provided error is a401
status code.
#
Returns
An array of query response data. The order returned is the same as the input order.
#
Throws
If an unmanaged error occur while performing any of the fetch requests, a GlobalDataQueriesError is thrown.
#
Usage
#
Define queries
A BootstrappingRoute
component is introduced in the following example because this hook must be rendered as a child of rootRoute
.
import { useProtectedDataQueries, useIsBootstrapping, AppRouter } from "@squide/firefly";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { ApiError, SessionContext, SubscriptionContext, type Session, type Subscription } from "@sample/shared";
function BootstrappingRoute() {
const [session, subscription] = useProtectedDataQueries([
{
queryKey: ["/api/session"],
queryFn: async () => {
const response = await fetch("/api/session");
if (!response.ok) {
throw new ApiError(response.status, response.statusText);
}
const data = await response.json();
const result: Session = {
user: {
id: data.userId,
name: data.username,
preferredLanguage: data.preferredLanguage
}
};
return result;
}
},
{
queryKey: ["/api/subscription"],
queryFn: async () => {
const response = await fetch("/api/subscription");
if (!response.ok) {
throw new ApiError(response.status, response.statusText);
}
return (await response.json()) as Subscription;
}
}
], error => isApiError(error) && error.status === 401);
if (useIsBootstrapping()) {
return <div>Loading...</div>;
}
return (
<SessionContext.Provider value={session}>
<SubscriptionContext.Provider value={subscription}>
<Outlet />
</SubscriptionContext.Provider>
</SessionContext.Provider>
);
}
export function App() {
return (
<AppRouter
waitForMsw
waitForProtectedData
>
{({ rootRoute, registeredRoutes, routerProviderProps }) => {
return (
<RouterProvider
router={createBrowserRouter([
{
element: rootRoute,
children: [
{
element: <BootstrappingRoute />,
children: registeredRoutes
}
]
}
])}
{...routerProviderProps}
/>
);
}}
</AppRouter>
);
}
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;
}
#
waitForProtectedData
& useIsBootstrapping
To ensure the AppRouter
component wait for the protected data to be ready before rendering the requested route, set the waitForProtectedData property to true
.
Combine this hook with the useIsBootstrapping hook to display a loader until the protected data is fetched and the application is ready.
#
Handle fetch errors
This hook throws GlobalDataQueriesError instances, which are typically unmanaged and should be handled by an error boundary. To assert 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 RootErrorBoundary() {
const error = useRouteError() as Error;
const location = useLocation();
const logger = useLogger();
useEffect(() => {
if (isGlobalDataQueriesError(error)) {
logger.error(`[shell] An unmanaged error occurred while rendering the route with path ${location.pathname}`, error.message, error.errors);
}
}, [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>
);
}
import { useProtectedDataQueries, useIsBootstrapping, AppRouter } from "@squide/firefly";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { ApiError, SessionContext, type Session } from "@sample/shared";
import { RootErrorBoundary } from "./RootErrorBoundary.tsx";
function BootstrappingRoute() {
const [session] = useProtectedDataQueries([
{
queryKey: ["/api/session"],
queryFn: async () => {
const response = await fetch("/api/session");
if (!response.ok) {
throw new ApiError(response.status, response.statusText);
}
const data = await response.json();
const result: Session = {
user: {
id: data.userId,
name: data.username,
preferredLanguage: data.preferredLanguage
}
};
return result;
}
}
], 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
waitForMsw
waitForProtectedData
>
{({ rootRoute, registeredRoutes, routerProviderProps }) => {
return (
<RouterProvider
router={createBrowserRouter([
{
element: rootRoute,
errorElement: <RootErrorBoundary />,
children: [
{
element: <BootstrappingRoute />,
children: registeredRoutes
}
]
}
])}
{...routerProviderProps}
/>
);
}}
</AppRouter>
);
}
#
Handle 401 response
Unauthorized requests are a special case that shouldn't be handled by an error boundary, as this would cause an infinite loop with the application's authentication boundary.
To handle this, when the server returns a 401
status code, the useProtectedDataQueries
hook instructs Squide to immediately render the page, triggering the authentication boundary, that will eventually redirect the user to a login page.
Since Squide manages this process behind the scenes, you only need to register an AuthenticationBoundary
component and provide an isUnauthorizedError
handler to the useProtectedDataQueries
hook.
import { useContext } from "react";
import { Outlet, Navigate } from "react-router-dom";
import { SessionContext } from "@sample/shared";
export function AuthenticationBoundary() {
const session = useContext(SessionContext);
if (session) {
return <Outlet />;
}
return <Navigate to="/login" />;
}
The registerHost
function is registered as a local module of the host application.
import { PublicRoutes, ProtectedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { AuthenticationBoundary } from "./AuthenticationBoundary.tsx";
export function registerHost() {
const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
runtime.registerRoute({
// Pathless route to declare an authenticated boundary.
element: <AuthenticationBoundary />,
children: [
PublicRoutes,
ProtectedRoutes
]
}, {
hoist: true
});
};
return register;
}
import { useProtectedDataQueries, useIsBootstrapping, AppRouter } from "@squide/firefly";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { ApiError, SessionContext, type Session } from "@sample/shared";
function BootstrappingRoute() {
const [session] = useProtectedDataQueries([
{
queryKey: ["/api/session"],
queryFn: async () => {
const response = await fetch("/api/session");
if (!response.ok) {
throw new ApiError(response.status, response.statusText);
}
const data = await response.json();
const result: Session = {
user: {
id: data.userId,
name: data.username,
preferredLanguage: data.preferredLanguage
}
};
return result;
}
}
], 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
waitForMsw
waitForProtectedData
>
{({ rootRoute, registeredRoutes, routerProviderProps }) => {
return (
<RouterProvider
router={createBrowserRouter([
{
element: rootRoute,
children: [
{
element: <BootstrappingRoute />,
children: registeredRoutes
}
]
}
])}
{...routerProviderProps}
/>
);
}}
</AppRouter>
);
}