#
Register deferred navigation items
Navigation items cannot always be registered before the application bootstrapping process, as some of them depend on remote data and/or feature flags.
To address this, Squide offers an alternate deferred registration mechanism in two-phases:
- The first phase allows modules to register their navigation items that are not dependent on remote data or feature flags.
- The second phase enables modules to register deferred navigation items that are dependent on remote data and/or feature flags by returning a function. We refer to this second phase as deferred registrations.
For more details, refer to the initializeFirefly and useDeferredRegistrations reference documentation.
#
Register a deferred item
To defer a registration to the second phase, a module's registration function can return an anonymous function matching the DeferredRegistrationFunction type: (data, operation: "register" | "update") => Promise | void.
#
Remote data
The returned registration function can conditionally register navigation items based on the remote data passed as its second argument:
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import type { DeferredRegistrationData } from "@sample/shared";
export const register: ModuleRegisterFunction<FireflyRuntime, unknown, DeferredRegistrationData> = runtime => {
// Once the user data has been loaded by the host application, by completing the module registrations process,
// the deferred registration function will be called with the user data.
return (deferredRuntime, { userData }) => {
// Only register the "feature-a" route and navigation item if the user is an administrator.
if (userData.isAdmin) {
deferredRuntime.registerNavigationItem({
$id: "feature-a",
$label: "Feature A",
to: "/feature-a"
});
}
};
};
#
Feature flags
And/or based on a LaunchDarkly feature flag:
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import type { DeferredRegistrationData } from "@sample/shared";
export const register: ModuleRegisterFunction<FireflyRuntime, unknown, DeferredRegistrationData> = runtime => {
// Once the user data has been loaded by the host application, by completing the module registrations process,
// the deferred registration function will be called with the user data.
return (deferredRuntime) => {
// Only register the "feature-a" route and navigation item if "feature-a" flag is activated.
if (deferredRuntime.getFeatureFlag("enable-feature-a")) {
deferredRuntime.registerNavigationItem({
$id: "feature-a",
$label: "Feature A",
to: "/feature-a"
});
}
};
};
export interface UserInfo {
isAdmin: boolean;
}
export interface DeferredRegistrationData {
userInfo?: UserInfo;
}
It's important to register conditional navigation items using the deferredRuntime argument rather than the root runtime argument.
#
Execute the deferred registrations
It's the responsibility of the application shell to execute deferred registrations. If deferred registrations depend on remote data, register them once that data has been retrieved:
import { AppRouter, useIsBootstrapping, usePublicDataQueries, useDeferredRegistrations } from "@squide/firefly";
import { createBrowserRouter, Outlet } from "react-router";
import { RouterProvider } from "react-router/dom";
import { DeferredRegistrationData, UserInfo } from "@sample/shared";
function BootstrappingRoute() {
const [userInfo] = usePublicDataQueries([
{
queryKey: ["/api/user-info"],
queryFn: async () => {
const response = await fetch("/api/user-info");
const data = await response.json();
const userInfo: UserInfo = {
isAdmin: data.isAdmin
};
return userInfo;
}
}
]);
// The useMemo hook is super important otherwise the hook will consider that the user info
// object changed everytime the hook is rendered.
const data: DeferredRegistrationData = useMemo(() => ({
userInfo
}), [userInfo]);
useDeferredRegistrations(data);
if (useIsBootstrapping()) {
return <div>Loading...</div>;
}
return <Outlet />;
}
export function App() {
return (
<AppRouter waitForPublicData>
{({ rootRoute, registeredRoutes, routerProviderProps }) => {
return (
<RouterProvider
router={createBrowserRouter([
{
element: rootRoute,
children: [
{
element: <BootstrappingRoute />,
children: registeredRoutes
}
]
}
])}
{...routerProviderProps}
/>
);
}}
</AppRouter>
);
}
Otherwise, the deferred registrations can be registered without providing a data object:
import { AppRouter, useIsBootstrapping, useDeferredRegistrations } from "@squide/firefly";
import { createBrowserRouter, Outlet } from "react-router";
import { RouterProvider } from "react-router/dom";
function BootstrappingRoute() {
useDeferredRegistrations();
if (useIsBootstrapping()) {
return <div>Loading...</div>;
}
return <Outlet />;
}
export function App() {
return (
<AppRouter>
{({ rootRoute, registeredRoutes, routerProviderProps }) => {
return (
<RouterProvider
router={createBrowserRouter([
{
element: rootRoute,
children: [
{
element: <BootstrappingRoute />,
children: registeredRoutes
}
]
}
])}
{...routerProviderProps}
/>
);
}}
</AppRouter>
);
}
#
Update deferred items
Since Squide integrates with TanStack Query and LaunchDarkly feature flags, and both regularly get fresh data from the server, the remote data or feature flags on which deferred navigation items depend may change over time. When this happens, the deferred navigation items must be updated to reflect the current state of the application. For example, a user could be promoted from a regular user to an administrator and should then see additional navigation items. Similarly, a feature flag might enable or disable a feature, which would require navigation items to be added or removed accordingly.
#
Remote data updates
By using the useDeferredRegistrations hook in combination with TanStack Query, deferred registrations are automatically updated whenever a fresh remote data object is forwarded to useDeferredRegistrations:
import { useIsBootstrapping, useDeferredRegistrations, usePublicDataQueries } from "@squide/firefly";
import { Outlet } from "react-router";
import { DeferredRegistrationData, UserInfo } from "@sample/shared";
function BootstrappingRoute() {
const [userInfo] = usePublicDataQueries([
{
queryKey: ["/api/user-info"],
queryFn: async () => {
const response = await fetch("/api/user-info");
const data = await response.json();
const userInfo: UserInfo = {
isAdmin: data.isAdmin
};
return userInfo;
}
}
]);
// The useMemo hook is super important otherwise the hook will consider that the user info
// object changed everytime the hook is rendered.
const data: DeferredRegistrationData = useMemo(() => ({
userInfo
}), [userInfo]);
useDeferredRegistrations(data);
if (useIsBootstrapping()) {
return <div>Loading...</div>;
}
return <Outlet />;
}
#
Feature flag updates
However, if conditional navigation items only depend on feature flags, the useDeferredRegistrations hook can be balled without a data object. Deferred registrations will still be updated automatically whenever a feature flag value changes:
import { useIsBootstrapping, useDeferredRegistrations } from "@squide/firefly";
import { Outlet } from "react-router";
function BootstrappingRoute() {
useDeferredRegistrations();
if (useIsBootstrapping()) {
return <div>Loading...</div>;
}
return <Outlet />;
}