# Learn the core pieces

Now that we've created a host application, loaded a few modules, registered routes and navigation items, and learn the modular design principles, let's delve into the core pieces provided by Squide.

# Runtime

The runtime instance serves as the backbone of a Squide application, enabling modularity and maintaining a decoupled and extendable system. It's purpose is to configure and manage the environment of the application, register the modules and plugins, and centralize cross-cutting concerns such as messaging, logging, telemetry/observability, environment variables and feature flags.

# Learn more

# Modular registration

Squide enables developers to build scalable modular applications with well-defined boundaries by allowing consumers to register dynamic routes, navigation items and MSW request handlers in modules. Each module contributes its own routing configuration, which the host application then assembles into unified structures at bootstrapping. This keeps routing, navigation and request handlers isolated within each module.

The opposite of a modular architecture is a tightly coupled architecture. This style of architecture often evolves into what is commonly described as a "big ball of mud". As the codebase grows, teams face increasing maintenance difficulties, scaling limitations, and unpredictable side effects.

Systems with highly coupled architectures are typically characterized by:

  • Unclear internal structure
  • Weak boundaries
  • High coupling
  • Low cohesion
  • Inconsistent design decisions

While such an architecture seems appropriate during the first weeks or months of a project, problems emerge quickly as the system grows:

  • Harder to understand: The codebase lacks boundaries and consistent patterns, making it difficult for developers to build a clear mental model. Logic becomes scattered across unrelated areas of the system.
  • Higher cost of change and risk of regressions: Modifying one part of the system often produces unexpected side effects elsewhere. The tight coupling increases the likelihood of regressions.
  • Slower onboarding: New developers take longer to understand the system due to the absence of clear boundaries and scattered logic.
  • Increased coordination needs: As more teams contribute to the same codebase, they frequently touch overlapping areas. This requires cross-team synchronization and generates coordination overhead.
  • Slower releases: Testing and releasing new features require validating the entire system, extending release cycles.
  • Unclear ownership: Without well-defined boundaries, ownership becomes ambiguous. This slows decision-making and complicates maintenance responsibilities.
  • Growing accidental complexity: Temporary workarounds accumulate rather than being replaced by proper abstractions, causing technical debt to rise quickly.
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { Page } from "./Page.tsx"

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

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

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

        runtime.registerRequestHandlers(requestHandlers);
    }
};

# Learn more

# Deferred registrations

Modules can delay registering navigation items until global data or feature flags are available by returning a registration function. That function is re-executed whenever global data or feature flags change, keeping the registrations up to date.

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

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

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

        runtime.registerRequestHandlers(requestHandlers);
    }

    return (deferredRuntime, { userData }) => {
        if (userData.isAdmin && deferredRuntime.getFeatureFlag("enable-page-1")) {
            runtime.registerNavigationItem({
                $id: "page-1",
                $label: "Page 1",
                to: "/page-1"
            });
        }
    };
};

# Learn more

# Public and protected pages

Modules can declare routes which are meant to be publicly accessible or that are protected (requires an authentication). Squide built-in primitives handles the bootstrapping orchestration to:

import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { PublicPage } from "./PublicPage.tsx"
import { ProtectedPage } from "./ProtectedPage.tsx"

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerPublicRoute({
        path: "/public",
        element: <PublicPage />
    });

    runtime.registerRoute({
        path: "/protected",
        element: <ProtectedPage />
    });
};

# Learn more

# Global data fetching

Squide makes global protected data fetching easier, by providing 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.
import { usePublicDataQueries, useProtectedDataQueries, useIsBootstrapping } from "@squide/firefly";
import { Outlet } from "react-router";
import { FetchCountContext, SubscriptionContext, isApiError } from "@sample/shared";

function BootstrappingRoute() {
    const [fetchCount] = usePublicDataQueries([
        {
            queryKey: ["/api/count"],
            queryFn: async () => {
                // ...
            }
        }
    ]);

    const [subscription] = useProtectedDataQueries([
        {
            queryKey: ["/api/subscription"],
            queryFn: async () => {
                // ...
            }
        }
    ], 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>
    );
}

# Learn more

# Messaging

Squide event bus offers a pub/sub mechanism for modules to communicate through events without creating tight coupling.

import { useCallback } from "react";
import { useEventBusListener } from "@squide/firefly";

const handleFoo = useCallback((data, context) => {
    // Do something...
}, []);

// Listen to the first "foo" event.
useEventBusListener("foo", handleFoo, { once: true };
import { useEventBusDispatcher } from "@squide/firefly";

const dispatch = useEventBusDispatcher();

// Dispatch a "foo" event with a "bar" payload.
dispatch("foo", "bar");

# Learn more

# Logging

Squide logger provides visibility into the application's bootstrapping flow and how modules behave and interact. It also offers an abstraction that allows applications to emit custom logs to multiple destinations defined by the host application.

import { useLogger } from "@squide/firefly";

const logger = useLogger();

logger.debug("Hello!");

# Learn more

# Environment variables

Squide attaches environment variables to a FireflyRuntime instance rather than accessing process.env throughout the codebase. This strategy supports a modular architecture and makes it easier to write tests and Storybook stories by isolating configuration from global state and making environment variables independent from build-time specifics.

import { initializeFirefly } from "@squide/firefly";

const runtime = initializeFirefly({
    environmentVariables: {
        apiBaseUrl: "https://my-api.com",
        telemetryBaseUrl: "https://my-telemetry.com"
    }
});
import { useEnvironmentVariable } from "@squide/firefly";

const variable = useEnvironmentVariable("apiBaseUrl");

# Learn more

# Feature flags

Squide integrates with LaunchDarkly to attach feature flags to the FireflyRuntime instance and automatically update deferred registrations whenever a flag value changes.

import { initializeFirefly } from "@squide/firefly";
import { initialize as initializeLaunchDarkly } from "launchdarkly-js-client-sdk";

const launchDarklyClient = initializeLaunchDarkly("123", {
    kind: "user",
    anonymous: true
}, {
    streaming: true
});

try {
    await launchDarklyClient.waitForInitialization(5);
} catch (error: unknown) {
    // Failed to initialize LaunchDarkly...
}

const runtime = initializeFirefly({
    launchDarklyClient
});
import { useFeatureFlag } from "@squide/firefly";

const value = useFeatureFlag("show-characters", true);

# Learn more

# Honeycomb

Squide integrates with @workleap/telemetry v2 or later (or @workleap/honeycomb v7 or later) to automatically send performance traces for the bootstrapping flow of an application.

import { FireflyProvider, initializeFirefly } from "@squide/firefly";
import { initializeTelemetry } from "@workleap/telemetry/react";
import { createRoot } from "react-dom/client";
import { App } from "./App.tsx";

const telemetryClient = initializeTelemetry({
    honeycomb: {
        namespace: "sample",
        serviceName: "squide-sample",
        apiServiceUrls: [/.+/g,],
        options: {
            proxy: "https://my-proxy.com"
        }
    }
});

const runtime = initializeFirefly({
    honeycombInstrumentationClient: telemetryClient.honeycomb
});

const root = createRoot(document.getElementById("root")!);

root.render(
    <FireflyProvider runtime={runtime}>
        <App />
    </FireflyProvider>
);

# Learn more

# Plugins

To keep Squide lightweight, not all functionalities should be integrated as a core functionality. However, to accommodate a broad range of technologies, a plugin system has been implemented to fill the gap.

# Learn more

import { initializeFirefly } from "@squide/firefly";
import { MyPlugin } from "@sample/my-plugin";

const runtime = initializeFirefly({
    plugins: [x => new MyPlugin(x)]
});

# Integrations

To finish setting up Squide, continue with the integration guides.

# Reference

For an exhaustive list of the Squide API, refer to the reference section.

# Samples

Finally, have a look at the sample applications to see the Squide API in action.