#
Federated tabs
While it's typically recommended for a Squide application to maintain the boundary of a page within a single domain, there are situations where enhancing the user experience necessitates rendering a page with parts from multiple domains, or at the very least, simulating it 😊.
For this guide, we'll take as an example a page for which the parts that are owned by different domains are organized by tabs (federated tabs):
Tab 1
: Registered byRemote Module 1
Tab 2
: Registered byRemote Module 2
Tab 3
: Registered byLocal Module
#
Define a nested layout
To construct this page while adhering to Squide constraint of exclusively permitting route and navigation items exports from modules, let's begin by defining a React Router nested layout. This nested layout will be responsible for rendering all the tab headers and the content of the active tab:
import { Suspense } from "react";
import { Link, Outlet } from "react-router-dom";
export function FederatedTabsLayout() {
return (
<div>
<p>Every tab is registered by a different module.</p>
<ul style={{ listStyleType: "none", margin: 0, padding: 0, display: "flex", gap: "20px" }}>
<li><Link to="federated-tabs/tab-1">Tab 1</Link></li>
<li><Link to="federated-tabs/tab-2">Tab 2</Link></li>
<li><Link to="federated-tabs/tab-3">Tab 3</Link></li>
</ul>
<div style={{ padding: "20px" }}>
<Suspense fallback={<div>Loading...</div>}>
<Outlet />
</Suspense>
</div>
</div>
);
}
In the previous code sample, the FederatedTabsLayout
is similar to the RootLayout
introduced in previous guides. However, the key distinction is that this layout is nested under the /federated-tabs
URL segment. By nesting the layout under a specific path, it will only render when the user navigates to one of the federated tab pages (e.g. /federated-tabs/tab-1
, /federated-tabs/tab-2
, /federated-tabs/tab-3
).
To register the newly created layout as a nested layout, use the registerRoute function:
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { FederatedTabsLayout } from "./FederatedTabsLayout.tsx";
export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
runtime.registerRoute({
// Register the layout as a nested layout under the "/federated-tabs" URL segment.
path: "/federated-tabs",
element: <FederatedTabsLayout />
});
runtime.registerNavigationItem({
$label: "Federated tabs",
to: "/federated-tabs"
});
}
With this nested layout in place, thanks to the React Router Outlet component, the content of the tabs can now reside in distinct pages (registered by different modules) while still delivering a cohesive user experience. Whenever a user navigates between the tabs, the URL will be updated, and the tab content will change, but the shared portion of the layout will remain consistent.
As a bonus, each individual tab will have its own dedicated URL! 🥳
It is recommended to define the shared layouts in a standalone package as it's done for the endpoints sample layouts project.
#
Create the tab routes
Next, let's add the actual tab pages to the modules. To do so, we'll use the parentPath option of the registerRoute function to register the routes under the FederatedTabsLayout
:
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { Tab1 } from "./Tab1.tsx";
export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
runtime.registerRoute({
// Using "index: true" instead of a path because this is the default active tab.
index: true
element: <Tab1 />
}, {
parentPath: "/federated-tabs"
});
}
export function Tab1() {
return (
<div>Hey, this is Tab 1 content</div>
);
}
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { Tab2 } from "./Tab2.tsx";
export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
runtime.registerRoute({
// React Router nested routes requires the first part of the "path" to be the same
// as the nested layout path (FederatedTabsLayout).
path: "/federated-tabs/tab-2"
element: <Tab2 />
}, {
parentPath: "/federated-tabs"
});
}
export function Tab2() {
return (
<div>Hey, this is Tab 2 content</div>
);
}
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { Tab3 } from "./Tab3.tsx";
export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
runtime.registerRoute({
// React Router nested routes requires the first part of the "path" to be the same
// as the nested layout path (FederatedTabsLayout).
path: "/federated-tabs/tab-3"
element: <Tab3 />
}, {
parentPath: "/federated-tabs"
});
}
export function Tab3() {
return (
<div>Hey, this is Tab 3 content</div>
);
}
Now that the tabs has been registered, ensure that all four modules (including remote-module-3
) are registered in the host application. Start the development servers using the dev
script. Navigate to the /federated-tabs
page, you should see the tab headers. Click on each tab header to confirm that the content renders correctly.
#
Decouple the navigation items
Althought it's functional, the modules are currently coupled by hardcoded URLs within the FederatedTabsLayout
.
To decouple the navigation items, similar to what is done for regular federated pages, we'll utilize the registerNavigationItem function. In this case, we'll also use the menuId option. Defining the menuId
option will enable the FederatedTabsLayout
to retrieve navigation items exclusively for the federated tab component.
First, let's register the navigation items with the menuId
option. For this example the menuId
will be /federated-tabs
(it can be anything):
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { Tab1 } from "./Tab1.tsx";
export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
runtime.registerRoute({
// Using "index: true" instead of a path because this is the default active tab.
index: true
element: <Tab1 />
}, {
parentPath: "/federated-tabs"
});
runtime.registerNavigationItem({
$label: "Tab 1",
to: "/federated-tabs"
}, {
// The menu id could be anything, in this example we are using the same path as the nested layout
// path for convenience.
menuId: "/federated-tabs"
});
}
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { Tab2 } from "./Tab2.tsx";
export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
runtime.registerRoute({
// React Router nested routes requires the first part of the "path" to be the same
// as the nested layout path (FederatedTabsLayout).
path: "/federated-tabs/tab-2"
element: <Tab2 />
}, {
parentPath: "/federated-tabs"
});
runtime.registerNavigationItem({
$label: "Tab 2",
to: "/federated-tabs/tab-2"
}, {
// The menu id could be anything, in this example we are using the same path as the nested layout
// path for convenience.
menuId: "/federated-tabs"
});
}
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { Tab3 } from "./Tab3.tsx";
export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
runtime.registerRoute({
// React Router nested routes requires the first part of the "path" to be the same
// as the nested layout path (FederatedTabsLayout).
path: "/federated-tabs/tab-3"
element: <Tab3 />
}, {
parentPath: "/federated-tabs"
});
runtime.registerNavigationItem({
$label: "Tab 3",
to: "/federated-tabs/tab-3"
}, {
// The menu id could be anything, in this example we are using the same path as the nested layout
// path for convenience.
menuId: "/federated-tabs"
});
}
Then, update the FederatedTabsLayout
to render the registered navigation items instead of the hardcoded URLs:
import {
useNavigationItems,
useRenderedNavigationItems,
type NavigationLinkRenderProps,
type RenderItemFunction,
type RenderSectionFunction
} from "@squide/react-router";
import { Suspense } from "react";
import { Link, Outlet } from "react-router-dom";
const renderItem: RenderItemFunction = (item, index, level) => {
const { label, linkProps } = item as NavigationLinkRenderProps;
return (
<li key={`${level}-${index}`}>
<Link {...linkProps}>
{label}
</Link>
</li>
);
};
const renderSection: RenderSectionFunction = elements => {
return (
<ul style={{ listStyleType: "none", margin: 0, padding: 0, display: "flex", gap: "20px" }}>
{elements}
</ul>
);
};
export function FederatedTabsLayout() {
const navigationItems = useNavigationItems({ menuId: "/federated-tabs" });
const renderedTabs = useRenderedNavigationItems(navigationItems, renderItem, renderSection);
return (
<div>
<p>Every tab is registered by a different module.</p>
{renderedTabs}
<div style={{ padding: "20px" }}>
<Suspense fallback={<div>Loading...</div>}>
<Outlet />
</Suspense>
</div>
</div>
);
}
#
Change the display order of the tabs
Similarly to how the display order of regular navigation items can be configured, a federated tab position can be affected with the priority property.
To force Tab 3
to be positioned first, we'll give him a priority of 999
:
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { Tab3 } from "./Tab3.tsx";
export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
runtime.registerRoute({
path: "/federated-tabs/tab-3"
element: <Tab3 />
}, {
parentPath: "/federated-tabs"
});
runtime.registerNavigationItem({
$label: "Tab 3",
// Highest priority goes first.
$priority: 999,
to: "/federated-tabs/tab-3"
}, {
menuId: "/federated-tabs"
});
}
#
Try it 🚀
To ensure everything is still working correctly, start the development servers using the dev
script and navigate to the /federated-tabs
page. You should see all three tabs, and you should be able to switch between them by clicking on the tab headers.
#
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] The following route has been registered as a children of the "/federated-tabs" route. Newly registered item: ...
[squide] The following navigation item has been registered to the "/federated-tabs" menu for a total of 1 item. Newly registered item: ...
- Refer to a working example on GitHub.
- Refer to the troubleshooting page.