# Fetch page data

There are various approaches to fetching data for pages. At Workleap, our preference is usually to develop a dedicated endpoint per page, returning a denormalized document specifically tailored for that page. We rely on server state as our singular source of truth and leverage React Query to manage data fetching and ensure our data remains up-to-date.

Although this approach works well, a few adjustments are necessary when transitioning from a monolithic application to a federated application.

# Install React Query

First, open a terminal at the root of the module and install the following packages:

pnpm add -D @tanstack/react-query-devtools
pnpm add @tanstack/react-query
yarn add -D @tanstack/react-query-devtools
yarn add @tanstack/react-query
npm install -D @tanstack/react-query-devtools
npm install @tanstack/react-query

# Setup the query client

Then, instanciate a QueryClient instance in the module registration function and wrap the routes element with a QueryClientProvider:

src/register.tsx
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Page } from "./Page.tsx";

// To minimize unexpected situations and faciliate maintenance, do not share the React Query cache.
// Instead create a distinct query client per module.
const queryClient = new QueryClient();

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        path: "/page",
        element: <QueryClientProvider client={queryClient}><Page /></QueryClientProvider>
    });

    runtime.registerNavigationItem({
        $label: "Page",
        to: "/page"
    });
}

# Create a component for providers

If the module register multiple routes, to prevent duplicating registration code, you can create a Providers component:

src/register.tsx
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Page } from "./Page.tsx";

// To minimize unexpected situations and faciliate maintenance, do not share the React Query cache.
// Instead create a distinct query client per module.
const queryClient = new QueryClient();

function Providers({ children }: { children: ReactNode }) {
    return (
        <QueryClientProvider client={queryClient}>
            {children}
        </QueryClientProvider>
    );
}

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        path: "/page",
        element: <Providers><Page /></Providers>
    });

    runtime.registerNavigationItem({
        $label: "Page",
        to: "/page"
    });
}

# Setup the development tools

To faciliate development, React Query provides devtools to help visualize all of the inner workings of React Query.

However, the React Query devtools has not been developed to handle a federated application with multiple QueryClient instances. To use the devtools, you must define a ReactQueryDevtools component for each QueryClient instance:

src/register.tsx
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { Page } from "./Page.tsx";

// To minimize unexpected situations and faciliate maintenance, do not share the React Query cache.
// Instead create a distinct query client per module.
const queryClient = new QueryClient();

function Providers({ children }: { children: ReactNode }) {
    return (
        <QueryClientProvider client={queryClient}>
            {children}
            <ReactQueryDevtools initialIsOpen={false} />
        </QueryClientProvider>
    );
}

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        path: "/page",
        element: <Providers><Page /></Providers>
    });

    runtime.registerNavigationItem({
        $label: "Page",
        to: "/page"
    });
}

Then, depending on which page of the application has been rendered, a distinct devtools instance will be accessible. For a better experience, we recommend activating the React Query devtools exclusively when developing a module in isolation:

src/register.tsx
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { Page } from "./Page.tsx";

// To minimize unexpected situations and faciliate maintenance, do not share the React Query cache.
// Instead create a distinct query client per module.
const queryClient = new QueryClient();

function Providers({ children }: { children: ReactNode }) {
    return (
        <QueryClientProvider client={queryClient}>
            {children}
            {process.env.ISOLATED && (
                <ReactQueryDevtools initialIsOpen={false} />
            )}
        </QueryClientProvider>
    );
}

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        path: "/page",
        element: <Providers><Page /></Providers>
    });

    runtime.registerNavigationItem({
        $label: "Page",
        to: "/page"
    });
}

# Fetch the page data

Now, let's fetch some data. First, add a Mock Service Worker (MSW) request handler to the local module:

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

export const requestHandlers: HttpHandler[] = [
    http.get("/api/characters", () => {
        return HttpResponse.json([{
            "id": 1,
            "name": "Rick Sanchez",
            "species": "Human"
        }, {
            "id": 2,
            "name": "Morty Smith",
            "species": "Human"
        }]);
    })
];

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

src/register.tsx
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);
    }
}

Then, update the Page component to fetch and render the data with useSuspenseQuery:

src/Page.tsx
import { useSuspenseQuery } from "@tanstack/react-query";

interface Character {
    id: number;
    name: string;
    species: string;
}

export function Page() {
    const { data: characters } = useSuspenseQuery({ queryKey: ["/api/characters"], queryFn: () => {
        const response = await fetch("/api/characters");
        const data = await response.json();

        return data;
    } });

    return (
        <div>
            {characters.map((x: Character) => {
                return (
                    <div key={x.id}>
                        <span>Id: {x.id}</span>
                        <span> - </span>
                        <span>Name: {x.name}</span>
                        <span> - </span>
                        <span>Species: {x.species}</span>
                    </div>
                );
            })}
        </div>
    );
}

# Define a fallback element

The previous code sample uses useSuspenseQuery instead of useQuery to fetch data. This enables an application to leverage a React Suspense boundary to render a fallback element in a layout component while the data is being fetched:

host/src/RootLayout.tsx
import { Suspense } from "react";
import { Outlet } from "react-router-dom";

export function RootLayout() {
    return (
        <Suspense fallback={<div>Loading...</div>}>
            <Outlet />
        </Suspense>
    );
}

# Try it 🚀

Start the local module in a development environment using the dev-isolated script. If you haven't completed the develop a module in isolation guide, use the dev script instead and skip the part about React Query devtools. Then, navigate to the /page page.

You should notice that the character's data is being fetch from the MSW request handler and rendered on the page. Additionally, you should notice that the React Query devtools are available (a ribbon at the bottom right corner).

# 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.