# Create an host application

Let's begin by creating the application that will serve as the entry point for our modular application and host the application modules.

# Install the packages

Create a new application (we'll refer to ours as host), then open a terminal at the root of the new solution and install the following packages:

pnpm add -D @workleap/rsbuild-configs @workleap/browserslist-config @rsbuild/core @rspack/core browserslist typescript @types/react @types/react-dom
pnpm add @squide/firefly react react-dom react-router @tanstack/react-query

# Setup the application

First, create the following files:

host
├── public
├──── index.html
├── src
├──── App.tsx
├──── RootLayout.tsx
├──── HomePage.tsx
├──── index.tsx
├──── register.tsx
├── .browserslistrc
├── rsbuild.dev.ts
├── rsbuild.build.ts
├── package.json

Then, ensure that you are developing your application using ESM syntax by specifying type: module in your package.json file:

host/package.json
{
    "type": "module"
}

# Module registration

Next, to register the modules, instanciate a shell FireflyRuntime instance. A local module will be registered in the next section of this quick start guide.

host/src/index.tsx
import { createRoot } from "react-dom/client";
import { ConsoleLogger, FireflyProvider, initializeFirefly, type RemoteDefinition } from "@squide/firefly";
import { App } from "./App.tsx";

const runtime = initializeFirefly({
    loggers: [x => new ConsoleLogger(x)]
});

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

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

Then, render the AppRouter component to define a React Router browser instance configured with the registered routes:

host/src/App.tsx
import { AppRouter } from "@squide/firefly";
import { createBrowserRouter } from "react-router";
import { RouterProvider } from "react-router/dom";

export function App() {
    return (
        <AppRouter>
            {({ rootRoute, registeredRoutes, routerProviderProps }) => {
                return (
                    <RouterProvider
                        router={createBrowserRouter([
                            {
                                element: rootRoute,
                                children: registeredRoutes
                            }
                        ])}
                        {...routerProviderProps}
                    />
                );
            }}
        </AppRouter>
    );
}

# Navigation items

Next, create a layout component to render the navigation items. In many applications, multiple pages often share a common layout that includes elements such as a navigation bar, a user profile menu, and a main content section. In a React Router application, this shared layout is commonly referred to as a RootLayout:

host/src/RootLayout.tsx
import { Suspense } from "react";
import { Link, Outlet } from "react-router/dom";
import { 
    useNavigationItems,
    useRenderedNavigationItems,
    isNavigationLink,
    type RenderItemFunction,
    type RenderSectionFunction
} from "@squide/firefly";

const renderItem: RenderItemFunction = (item, key) => {
    // To keep thing simple, this sample doesn't support nested navigation items.
    // For an example including support for nested navigation items, have a look at
    // https://workleap.github.io/wl-squide/reference/routing/userenderednavigationitems/
    if (!isNavigationLink(item)) {
        return null;
    }

    const { label, linkProps, additionalProps } = item;

    return (
        <li key={key}>
            <Link {...linkProps} {...additionalProps}>
                {label}
            </Link>
        </li>
    );
};

const renderSection: RenderSectionFunction = (elements, key) => {
    return (
        <ul key={key}>
            {elements}
        </ul>
    );
};

export function RootLayout() {
    // Retrieve the navigation items registered by the modules.
    const navigationItems = useNavigationItems();

    // Transform the navigation items into React elements.
    const navigationElements = useRenderedNavigationItems(navigationItems, renderItem, renderSection);

    return (
        <>
            <nav>{navigationElements}</nav>
            <Suspense fallback={<div>Loading...</div>}>
                <Outlet />
            </Suspense>
        </>
    );
}

The RootLayout component created in the previous sample will serves as the default layout for the homepage as well as for every page (route) registered by a module that are not nested under a parent route with either the parentPath or the parentId option.

# Homepage

Next, create the HomePage component that will serve as the homepage:

host/src/HomePage.tsx
export function HomePage() {
    return (
        <div>Hello from the Home page!</div>
    );
}

Then, add a local module at the root of the host application to register the homepage:

host/src/register.tsx
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { HomePage } from "./HomePage.tsx";

export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        index: true,
        element: <HomePage />
    });
};

And an hoisted route to render the RootLayout with the PublicRoutes and ProtectedRoutes placeholders:

host/src/register.tsx
import { PublicRoutes, ProtectedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { HomePage } from "./HomePage.tsx";
import { RootLayout } from "./RootLayout.tsx";

export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        // Pathless route to declare a root layout.
        element: <RootLayout />,
        children: [
            // Placeholders indicating where non hoisted or nested public and protected routes will be rendered.
            PublicRoutes,
            ProtectedRoutes
        ]
    }, {
        hoist: true
    });

    runtime.registerRoute({
        index: true,
        element: <HomePage />
    });
};

Finally, update the bootstrapping code to register the newly created local module:

host/src/index.tsx
import { createRoot } from "react-dom/client";
import { ConsoleLogger, FireflyProvider, initializeFirefly, type RemoteDefinition } from "@squide/firefly";
import { registerHost } from "./register.tsx";
import { App } from "./App.tsx";

const runtime = initializeFirefly({
    localModules: [registerHost, registerMyLocalModule],
    loggers: [x => new ConsoleLogger(x)]
});

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

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

# Not found page (404)

Now, let's ensure that users who enter a wrong URL end up somewhere by registering a custom no-match route. First, create the NotFoundPage component, which will serve as the page for handling not found routes:

host/src/NotFoundPage.tsx
export function NotFoundPage() {
    return (
        <div>Not found! Please try another page.</div>
    );
}

Then, register the newly created component as the * route:

host/src/register.tsx
import { PublicRoutes, ProtectedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { HomePage } from "./HomePage.tsx";
import { NotFoundPage } from "./NotFoundPage.tsx";
import { RootLayout } from "./RootLayout.tsx";

export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        element: <RootLayout />,
        children: [
            // Placeholders indicating where non hoisted or nested public and protected routes will be rendered.
            PublicRoutes,
            ProtectedRoutes
        ]
    }, {
        hoist: true
    });

    runtime.registerPublicRoute({
        path: "*",
        element: <NotFoundPage />
    });

    runtime.registerRoute({
        index: true,
        element: <HomePage />
    });
};

# Configure Rsbuild

First, open the public/index.html file created at the beginning of this guide and copy/paste the following template:

host/public/index.html
<!DOCTYPE html>
<html>
    <head>
    </head>
    <body>
        <div id="root"></div>
    </body>
</html>

Then, open the .browserslist file and copy/paste the following content:

host/.browserslistrc
extends @workleap/browserslist-config

# Development configuration

To configure Rsbuild for a development environment, open the rsbuild.dev.ts file and use the defineDevConfig function to configure Rsbuild:

host/rsbuild.dev.ts
import { defineDevConfig } from "@workleap/rsbuild-configs";

export default defineDevConfig();

# Build configuration

To configure Rsbuild for a build environment, open the rsbuild.build.ts file and use the defineBuildConfig function to configure Rsbuild:

host/rsbuild.build.ts
import { defineBuildConfig } from "@workleap/rsbuild-configs";

export default defineBuildConfig();

# Add CLI scripts

To initiate the development server, add the following script to the application package.json file:

host/package.json
{
    "dev": "rsbuild dev --config ./rsbuild.dev.ts"
}

To build the application, add the following script to the application package.json file:

host/package.json
{
    "build": "rsbuild build --config rsbuild.build.ts"
}

# Try it 🚀

Start the application in a development environment using the dev script. You should see the homepage.

# Troubleshoot issues

If you are experiencing issues with this guide:

  • Open the DevTools console. You'll find a log entry for each registration that occurs and error messages if something went wrong:
    • [squide] Found 1 local module to register.
    • [squide] 1/1 Registering local module.
    • [squide] 1/1 Local module registration completed.
  • Refer to a working example on GitHub.
  • Refer to the troubleshooting page.