#
Fetch global data
Before going forward with this guide, make sure that you completed the Setup Mock Service Worker guide.
Retrieving the global data of an application is a crucial aspect that isn't always straightforward to implement. That's why we encourage feature teams to build their global data fetching strategy on top of the Squide AppRouter component.
#
Challenges with global data
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 Squide 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 (including the remote 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.
To help manage those concerns, Squide offer an AppRouter
component that takes care of setuping Squide's primitive and orchestrating the different states.
#
Fetch public data
#
Add an endpoint
First, define in the host application an MSW request handler that returns the number of times it has been fetched:
import { HttpResponse, http, type HttpHandler } from "msw";
let fetchCount = 0;
export const requestHandlers: HttpHandler[] = [
http.get("/api/count", () => {
fetchCount += 1;
return HttpResponse.json([{
"count": fetchCount
}]);
})
];
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 stuff to the application bundles.
const requestHandlers = (await import("../mocks/handlers.ts")).requestHandlers;
runtime.registerRequestHandlers(requestHandlers);
}
}
#
Create a shared context
Then, in a shared project, create a React context named FetchCountContext
:
import { createContext, useContext } from "react";
export const FetchCountContext = createContext(0);
export function useFetchCount() {
return useContext(FetchCountContext);
}
Ensure that the shared project is configured as a shared dependency.
#
Create a custom error class
Then, in the same shared project, create a ApiError
class:
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;
}
#
Fetch the data
Finally, update the App
component to add the usePublicDataQueries hook. The hook will fetch the data from /api/count
and forward the retrieved fetchCount
value through FetchCountContext
:
import { AppRouter, usePublicDataQueries, useIsBootstrapping } from "@squide/firefly";
import { RouterProvider, createBrowserRouter, Outlet } from "react-router-dom";
import { FetchCountContext, ApiError } from "@sample/shared";
function BootstrappingRoute() {
const [fetchCount] = usePublicDataQueries([
{
queryKey: ["/api/count"],
queryFn: async () => {
const response = await fetch("/api/count");
if (!response.ok) {
throw new ApiError(response.status, response.statusText);
}
const data = await response.json();
return data.count as number;
}
}
]);
if (useIsBootstrapping()) {
return <div>Loading...</div>;
}
return (
<FetchCountContext.Provider value={fetchCount}>
<Outlet />
</FetchCountContext.Provider>
);
}
export function App() {
return (
<AppRouter
waitForMsw
waitForPublicData
>
{({ rootRoute, registeredRoutes, routerProviderProps }) => {
return (
<RouterProvider
router={createBrowserRouter([
{
element: rootRoute,
children: [
{
element: <BootstrappingRoute />,
children: registeredRoutes
}
]
}
])}
{...routerProviderProps}
/>
);
}}
</AppRouter>
);
}
#
usePublicDataQueries
The usePublicDataQueries
hook is a wrapper around TanStack Query's native useQueries hook. This wrapper coordinates the execution of the queries with the AppRouter
component's state.
#
waitForPublicData
& useIsBootstrapping
To ensure the AppRouter
component wait for the public data to be ready before rendering the requested route, set the waitForPublicData property to true
.
Combine the usePublicDataQueries
with the useIsBootstrapping hook to display a loader until the public data is fetched and the application is ready.
#
Use the endpoint data
Now, create a GlobalDataLayout
component that uses the count retrieved from FetchCountContext
and render pages with a green background color if the value is odd:
import { useFetchCount } from "@sample/shared";
import { Outlet } from "react-router-dom";
export function GlobalDataLayout() {
const fetchCount = useFetchCount();
const isOdd = fetchCount % 2 === 0;
return (
<div style={{ backgroundColor: isOdd ? "green" : undefined }}>
<Outlet />
</div>
)
}
Then, create a Page
component:
export function Page() {
return (
<div>When the fetch count is odd, my background should be green.</div>
)
}
Finally, register both components, either in the host application or within any module:
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { GlobalDataLayout } from "./GlobalDataLayout.tsx";
import { Page } from "./Page.tsx";
export const register: ModuleRegisterFunction<FireflyRuntime> = async runtime => {
runtime.registerRoute({
path: "/global-data",
element: <GlobalDataLayout />,
children: [
{
index: true,
element: <Page />
}
]
});
runtime.registerNavigationItem({
$id: "global-data",
$label: "Global data Page",
to: "/global-data"
});
// Files that includes an import to the "msw" package are included dynamically to prevent adding
// unused MSW stuff to the application bundles.
const requestHandlers = (await import("../mocks/handlers.ts")).requestHandlers;
runtime.registerRequestHandlers(requestHandlers);
}
#
Try it 🚀
Start the application in a development environment using the dev
script and navigate to the /global-data
page. Refresh the page a few times, the background color should alternate between transparent and green.
#
Troubleshoot issues
If you are experiencing issues with this section of the guide:
- Open the DevTools console. You'll find a log entry for each registration that occurs (including MSW request handlers) and error messages if something went wrong.
- Refer to a working example on GitHub.
- Refer to the troubleshooting page.
#
Fetch protected data
Now, let's load protected data. The process is similar to fetching public data, but this time, we'll use the useProtectedDataQueries hook instead.
#
Add an endpoint
First, define a MSW request handler that returns a user tenant subscription data:
import { HttpResponse, http, type HttpHandler } from "msw";
let fetchCount = 0;
export const requestHandlers: HttpHandler[] = [
http.get("/api/count", () => {
fetchCount += 1;
return HttpResponse.json([{
"count": fetchCount
}]);
}),
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"
}]);
})
];
If you've registered the
#
Create a shared context
Then, in a shared project, create a SubscriptionContext
:
import { createContext, useContext } from "react";
export interface Subscription {
status: string
}
export const SubscriptionContext = createContext(Subscription | undefined);
export function useSubscription() {
return useContext(SubscriptionContext);
}
If you're not adding the SubscriptionContext
to the shared project created earlier for the public data example, make sure to configure this new shared project as a shared dependency.
#
Fetch the data
Finally, update the App
component to add the useProtectedDataQueries hook. The hook will fetch the data from /api/subscription
and forward the retrieved subscription
data through SubscriptionContext
:
import { AppRouter, usePublicDataQueries, useProtectedDataQueries, useIsBootstrapping } from "@squide/firefly";
import { RouterProvider, createBrowserRouter, Outlet } from "react-router-dom";
import { FetchCountContext, SubscriptionContext, ApiError, isApiError, type Subscription } from "@sample/shared";
function BootstrappingRoute() {
const [fetchCount] = usePublicDataQueries([
{
queryKey: ["/api/count"],
queryFn: async () => {
const response = await fetch("/api/count");
if (!response.ok) {
throw new ApiError(response.status, response.statusText);
}
const data = await response.json();
return data.count as number;
}
}
]);
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();
const subscription: Subscription = {
status: data.status
};
return subscription;
}
}
], error => isApiError(error) && error.status === 401);
if (useIsBootstrapping()) {
return <div>Loading...</div>;
}
return (
<FetchCountContext.Provider value={fetchCount}>
<SubscriptionContext.Provider value={subscription}>
<Outlet />
</SubscriptionContext.Provider>
</FetchCountContext.Provider>
);
}
export function App() {
return (
<AppRouter
waitForMsw
waitForPublicData
waitForProtectedData
>
{({ rootRoute, registeredRoutes, routerProviderProps }) => {
return (
<RouterProvider
router={createBrowserRouter([
{
element: rootRoute,
children: [
{
element: <BootstrappingRoute />,
children: registeredRoutes
}
]
}
])}
{...routerProviderProps}
/>
);
}}
</AppRouter>
);
}
#
useProtectedDataQueries
The useProtectedDataQueries
hook is a wrapper around TanStack Query's native useQueries hook. This wrapper coordinates the execution of the queries with the AppRouter
component's state.
#
waitForProtectedData
To ensure the AppRouter
component wait for the protected data to be ready before rendering the requested route, set the waitForProtectedData property to true
.
#
Use the endpoint data
Now, update the GlobalDataLayout
component that was previously created for the
import { useFetchCount, useSubscription } from "@sample/shared";
import { Outlet } from "react-router-dom";
export function GlobalDataLayout() {
const fetchCount = useFetchCount();
const subscription = useSubscription();
const isOdd = fetchCount % 2 === 0;
return (
<div>Subscription status: {subscription?.status}</div>
<div style={{ backgroundColor: isOdd ? "green" : undefined }}>
<Outlet />
</div>
)
}
#
Try it 🚀
Start the application in a development environment using the dev
script and navigate to the /global-data
page. You should notice the subscription status.
#
Troubleshoot issues
If you are experiencing issues with this section of the guide:
- Open the DevTools console. You'll find a log entry for each registration that occurs (including MSW request handlers) and error messages if something went wrong.
- Refer to a working example on GitHub.
- Refer to the troubleshooting page.