#
Create an host application
Use an existing template
We highly recommend going through the entire getting started guide. However, if you prefer to scaffold the application we'll be building, a template is available with degit:
corepack pnpx degit https://github.com/workleap/wl-squide/templates/getting-started
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:
{
"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.
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:
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
:
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:
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:
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:
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 />
});
};
The PublicRoutes and ProtectedRoutes placeholders indicates where routes that are neither hoisted or nested with a parentPath or parentId option will be rendered. In this example, the homepage route is considered as a protected route and will be rendered under the ProtectedRoutes
placeholder.
Finally, update the bootstrapping code to register the newly created local module:
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:
export function NotFoundPage() {
return (
<div>Not found! Please try another page.</div>
);
}
Then, register the newly created component as the *
route:
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
For additional information about this Rsbuild setup, refer to the development and production configuration documentation of the Web Configs libraries.
First, open the public/index.html
file created at the beginning of this guide and copy/paste the following template:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="root"></div>
</body>
</html>
Then, open the .browserslist
file and copy/paste the following content:
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:
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:
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:
{
"dev": "rsbuild dev --config ./rsbuild.dev.ts"
}
To build the application, add the following script to the application package.json
file:
{
"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.