#
Migrate to firefly v9.0
This major version of @squide/firefly
introduces TanStack Query as the official library for fetching the global data of a Squide's application and features a complete rewrite of the AppRouter component, which now uses a state machine to manage the application's bootstrapping flow.
Prior to v9.0
, Squide applications couldn't use TanStack Query to fetch global data, making it challenging for Workleap's applications to keep their global data in sync with the server state. With v9.0
, applications can now leverage custom wrappers of the TanStack Query's useQueries hook to fetch and keep their global data up-to-date with the server state. Additionally, the new deferred registrations update feature allows applications to even keep their conditional navigation items in sync with the server state.
Finally, with v9.0
, Squide's philosophy has evolved. We used to describe Squide as a shell for federated applications. Now, we refer to Squide as a shell for modular applications. After playing with Squide's local module feature for a while, we discovered that Squide offers significant value even for non-federated applications, which triggered this shift in philosophy.
#
Breaking changes
#
Removed
- The
useAreModulesRegistered
hook has been removed, use the useIsBootstrapping hook instead. - The
useAreModulesReady
hook has been removed, use the useIsBootstrapping hook instead. - The
useIsMswStarted
hook has been removed, use the useIsBootstrapping hook instead. - The
completeModuleRegistrations
function as been removed use the useDeferredRegistrations hook instead. - The
completeLocalModulesRegistrations
function has been removed use the useDeferredRegistrations hook instead. - The
completeRemoteModuleRegistrations
function has been removed use the useDeferredRegistrations hook instead. - The
useSession
hook has been removed, define your own React context instead. - The
useIsAuthenticated
hook has been removed, define your own React context instead. - The
sessionAccessor
option has been removed from the FireflyRuntime options, define your own React context instead. - The
ManagedRoutes
placeholder has been removed, use PublicRoutes and ProtectedRoutes instead.
#
Renamed
- The
setMswAsStarted
function has been renamed to setMswIsReady. - A route definition
$name
option has been renamed to $id. - The registerRoute
parentName
option has been renamed to parentId.
#
Others
- The
@squide/firefly
package now takes a peerDependency on@tanstack/react-query
. - The
@squide/firefly
package doesn't takes a peerDependency onreact-error-boundary
anymore.
#
Removed support for deferred routes
Deferred registration functions no longer support route registration; they are now exclusively used for registering navigation items. Since deferred registration functions can now be re-executed whenever the global data changes, registering routes in deferred registration functions no longer makes sense as updating the routes registry after the application has bootstrapped could lead to issues.
This change is a significant improvement for Squide's internals, allowing us to eliminate quirks like:
Treating unknown routes as
protected
: When a user initially requested a deferred route, Squide couldn't determine if the route waspublic
orprotected
because it wasn't registered yet. As a result, for that initial request, the route was consideredprotected
, even if the deferred registration later registered it aspublic
.Mandatory wildcard
*
route registration: Previously, Squide's bootstrapping would fail if the application didn't include a wildcard route.
Before:
export const register: ModuleRegisterFunction<FireflyRuntime, unknown, DeferredRegistrationData> = runtime => {
return ({ featureFlags }) => {
if (featureFlags?.featureB) {
runtime.registerRoute({
path: "/page",
element: <Page />
});
runtime.registerNavigationItem({
$id: "page",
$label: "Page",
to: "/page"
});
}
};
}
Now:
export const register: ModuleRegisterFunction<FireflyRuntime, unknown, DeferredRegistrationData> = runtime => {
runtime.registerRoute({
path: "/page",
element: <Page />
});
return ({ featureFlags }) => {
if (featureFlags?.featureB) {
runtime.registerNavigationItem({
$id: "page",
$label: "Page",
to: "/page"
});
}
};
}
#
Conditional routes
To handle direct access to a conditional route, each conditional route's endpoint should return a 403
status code if the user is not authorized to view the route. Those 403
errors should then be handled by the nearest error boundary.
#
Plugin's constructors now requires a runtime instance
Prior to this release, plugin instances received the current runtime instance through a _setRuntime
function. This approach caused issues because some plugins required a reference to the runtime at instantiation. To address this, plugins now receive the runtime instance directly as a constructor argument.
Before:
export class MyPlugin extends Plugin {
readonly #runtime: Runtime;
constructor() {
super(MyPlugin.name);
}
_setRuntime(runtime: Runtime) {
this.#runtime = runtime;
}
}
Now:
export class MyPlugin extends Plugin {
constructor(runtime: Runtime) {
super(MyPlugin.name, runtime);
}
}
#
Plugins now registers with a factory function
Prior to this release, the FireflyRuntime accepted plugin instances as options. Now, FireflyRuntime
accepts factory functions instead of plugin instances. This change allows plugins to receive the runtime instance as a constructor argument.
Before:
const runtime = new FireflyRuntime({
plugins: [new MyPlugin()]
});
Now:
const runtime = new FireflyRuntime({
plugins: [x => new MyPlugin(x)]
});
#
Rewrite of the AppRouter
component
This release features a full rewrite of the AppRouter component. The AppRouter
component used to handle many concerns like global data fetching, deferred registrations, error handling and a loading state. Those concerns have been delegated to the consumer code, supported by the new useIsBootstrapping, usePublicDataQueries, useProtectedDataQueries and useDeferredRegistrations hooks.
Before:
export function App() {
const [featureFlags, setFeatureFlags] = useState<FeatureFlags>();
const [subscription, setSubscription] = useState<FeatureFlags>();
const handleLoadPublicData = useCallback((signal: AbortSignal) => {
return fetchPublicData(setFeatureFlags, signal);
}, []);
const handleLoadProtectedData = useCallback((signal: AbortController) => {
return fetchProtectedData(setSubscription, signal);
}, []);
const handleCompleteRegistrations = useCallback(() => {
return completeModuleRegistrations(runtime, {
featureFlags,
subscription
});
}, [runtime, featureFlags, subscription]);
return (
<AppRouter
fallbackElement={<div>Loading...</div>}
errorElement={<RootErrorBoundary />}
waitForMsw
onLoadPublicData={handleLoadPublicData}
onLoadProtectedData={handleLoadProtectedData}
isPublicDataLoaded={!!featureFlags}
isPublicDataLoaded={!!subscription}
onCompleteRegistrations={handleCompleteRegistrations}
>
{(routes, providerProps) => (
<RouterProvider router={createBrowserRouter(routes)} {...providerProps} />
)}
</AppRouter>
);
}
Now:
function BootstrappingRoute() {
const [featureFlags] = usePublicDataQueries([getFeatureFlagsQuery]);
const [subscription] = useProtectedDataQueries([getSubscriptionQuery]);
const data: DeferredRegistrationData = useMemo(() => ({
featureFlags,
subscription
}), [featureFlags, subscription]);
useDeferredRegistrations(data);
if (useIsBootstrapping()) {
return <div>Loading...</div>;
}
return <Outlet />;
}
export function App() {
return (
<AppRouter waitForMsw waitForPublicData>
{({ rootRoute, registeredRoutes, routerProviderProps }) => {
return (
<RouterProvider
router={createBrowserRouter([
{
element: rootRoute,
errorElement: <RootErrorBoundary />,
children: [
{
element: <BootstrappingRoute />,
children: registeredRoutes
}
]
}
])}
{...routerProviderProps}
/>
);
}}
</AppRouter>
);
}
#
New hooks and functions
- A new useIsBoostrapping hook is now available.
- A new useDeferredRegistrations hook is now available.
- A new usePublicDataQueries hook is now available.
- A new useProtectedDataQueries hook is now available.
- A new isGlobalDataQueriesError function is now available.
- A new registerPublicRoute function is now available.
#
Improvements
- Deferred registration functions now always receive a
data
argument. - Deferred registration functions now receives a new operations argument.
- Navigation items now include a $canRender option, enabling modules to control whether a navigation item should be rendered.
#
New $id
option for navigation items
Navigation items now supports a new $id
option. Previously, most navigation item React elements used a key
property generated by concatenating the item's level
and index
, which goes against React's best practices:
<li key={`${level}-${index}`}>
It wasn't that much of a big deal since navigation items never changed once the application was bootstrapped. Now, with the deferred registration functions re-executing when the global data changes, the registered navigation items can be updated post-bootstrapping. The new $id
option allows the navigation item to be configured with a unique key at registration, preventing UI shifts.
runtime.registerNavigationItem({
$id: "page-1",
$label: "Page 1",
to: "/page-1"
});
The configured $id
option is then passed as a key
argument to the useRenderedNavigationItems rendering functions:
const renderItem: RenderItemFunction = (item, key) => {
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>
);
};
const navigationElements = useRenderedNavigationItems(navigationItems, renderItem, renderSection);
If no
$id
is configured for a navigation item, thekey
argument will be a concatenation of thelevel
andindex
argument.
#
Migrate an host application
A migration example from v8 to v9 is available for the wl-squide-monorepo-template.
The v9.0
release introduces several breaking changes affecting the host application code. Follow these steps to migrate an existing host application:
- Add a dependency to
@tanstack/react-query
. View example - Transition to the new
AppRouter
component.View example onLoadPublicData
+isPublicDataLoaded
becomes usePublicDataQueriesonLoadProtectedData
+isProtectedDataLoaded
becomes useProtectedDataQueriesonCompleteRegistrations
becomes useDeferredRegistrationsfallbackElement
becomes useIsBootstrappingerrorElement
is removed and somewhat replaced by aroot error boundary
- Create a
TanStackSessionManager
class and theSessionManagerContext
. Replace the session's deprecated hooks by creating the customsuseSession
anduseIsAuthenticated
hooks. View example - Remove the
sessionAccessor
option from theFireflyRuntime
instance. Update theBootstrappingRoute
component to create aTanStackSessionManager
instance and share it down the component tree using aSessionManagedContext
provider. View example - Add or update the
AuthenticationBoundary
component to use the newuseIsAuthenticated
hook. Global data fetch request shouldn't be throwing 401 error anymore when the user is not authenticated. View example - Update the
AuthenticatedLayout
component to use the session manager instance to clear the session. Retrieve the session manager instance from the context defined in theBootstrappingRoute
component using theuseSessionManager
hook. View example - Update the
AuthenticatedLayout
component to use the newkey
argument.View example - Replace the
ManagedRoutes
placeholder with the new PublicRoutes and ProtectedRoutes placeholders. View example - Convert all deferred routes into static routes.
View example - Add an
$id
option to the navigation item registrations.View example
#
Root error boundary
When transitioning to the new AppRouter
component, make sure to nest the RootErrorBoundary
component within the AppRouter
component's render function.
Before:
export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
runtime.registerRoute({
element: <RootLayout />,
children: [
{
$id: "root-error-boundary",
errorElement: <RootErrorBoundary />,
children: [
ManagedRoutes
]
}
]
});
});
Now:
export function App() {
return (
<AppRouter waitForMsw>
{({ rootRoute, registeredRoutes, routerProviderProps }) => {
return (
<RouterProvider
router={createBrowserRouter([
{
element: rootRoute,
errorElement: <RootErrorBoundary />,
children: registeredRoutes
}
])}
{...routerProviderProps}
/>
);
}}
</AppRouter>
);
}
#
Migrate a module
A migration example from v8 to v9 is available for the wl-squide-monorepo-template.
The changes in v9.0
have minimal impact on module code. To migrate an existing module, follow these steps:
- Convert all deferred routes into static routes.
View example - Add a
$id
option to the navigation item registrations.View example
#
Isolated development
If your module is set up for isolated development, ensure that you also apply the