#
Migrate from v8.* to v12.0
This migration guide is an aggregation of all the changes that happened between Squide Firefly v9.0
and v12.0
:
#
Changes summary
#
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.
#
v9.3
This minor version deprecate the registerLocalModules, registerRemoteModules and setMswAsReady in favor of a bootstrap
function.
#
v10.0
This major version introduces support for React Router v7
. The peer dependencies for @squide/firefly
and @squide/react-router
have been updated from react-router-dom@6*
to react-router@7*
and the React Router shared dependency name has been renamed from react-router-dom
to react-router
for @squide/firefly-webpack-configs
and @squide/firefly-rsbuild-configs
.
#
v11.0
This major version transform the bootstrap
function from an async function a sync function. It also introduces a new FireflyProvider alias for RuntimeContext.Provider
.
#
v12.0
This major version introduces a new initializeFirefly function, replacing the bootstrap
function. This new initializeFirefly
function is similar the previous bootstrap
function with the addition that it takes care of creating and returning a Runtime instance.
This major version introduces a new initializeFirefly function that replaces the legacy bootstrap
function. In addition to providing similar functionality, initializeFirefly
creates and returns a Runtime instance.
#
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.
#
Dependencies updates
- The
@squide/firefly
package now has a peerDependency on@tanstack/react-query
. - The
@squide/firefly
package doesn't have a peerDependency onreact-error-boundary
anymore. - The
@squide/firefly
package doesn't supportreact-router-dom@6*
anymore, remove thereacy-router-dom
dependency and update toreact-router@7*
.
#
Deprecation
- The registerLocalModules function has been deprecated, use the
bootstrap
function instead. - The registerRemoteModules function has been deprecated, use the
bootstrap
function instead. - The setMswAsReady function has been deprecated, use the
bootstrap
function instead. - The
RuntimeContext.Provider
has been deprecated, use FireflyProvider instead.
#
Removed support for deferred routes
As of v9.0
, 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 v9.0
, 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 v9.0
, the FireflyRuntime accepted plugin instances as options. Now plugins should be registered with the initializeFirefly function which accepts factory functions instead of plugin instances. This change allows plugins to receive the runtime instance as a constructor argument.
Before:
import { FireflyRuntime } from "@squide/firefly";
const runtime = new FireflyRuntime({
plugins: [new MyPlugin()]
});
Now:
import { initializeFirefly } from "@squide/firefly";
const runtime = initializeFirefly({
plugins: [x => new MyPlugin(x)]
});
#
Rewrite of the AppRouter
component
v9.0
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:
import { initializeFirefly } from "@squide/firefly";
const runtime = initializeFirefly({
useMsw: true
});
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 waitForPublicData>
{({ rootRoute, registeredRoutes, routerProviderProps }) => {
return (
<RouterProvider
router={createBrowserRouter([
{
element: rootRoute,
errorElement: <RootErrorBoundary />,
children: [
{
element: <BootstrappingRoute />,
children: registeredRoutes
}
]
}
])}
{...routerProviderProps}
/>
);
}}
</AppRouter>
);
}
#
Use the initializeFirefly
function
Versions v9.3
, v11.0
and v12.0
introduce changes to how the FireflyRuntime instance should be created and how the modules should be registered.
Before:
import { ConsoleLogger, FireflyProvider, FireflyRuntime, registerRemoteModules, registerLocalModules, type RemoteDefinition } from "@squide/firefly";
import { register as registerMyLocalModule } from "@getting-started/local-module";
import { createRoot } from "react-dom/client";
import { App } from "./App.tsx";
import { registerHost } from "./register.tsx";
// Define the remote modules.
const Remotes: RemoteDefinition[] = [
{ name: "remote1" }
];
// Create the shell runtime.
const runtime = new FireflyRuntime({
loggers: [x => new ConsoleLogger(x)]
});
// Register the local module.
await registerLocalModules([registerHost, registerMyLocalModule], runtime);
// Register the remote module.
await registerRemoteModules(Remotes, runtime);
const root = createRoot(document.getElementById("root")!);
root.render(
<FireflyProvider runtime={runtime}>
<App />
</FireflyProvider>
);
Now:
import { ConsoleLogger, FireflyProvider, FireflyRuntime, initializeFirefly, type RemoteDefinition } from "@squide/firefly";
import { register as registerMyLocalModule } from "@getting-started/local-module";
import { createRoot } from "react-dom/client";
import { App } from "./App.tsx";
import { registerHost } from "./register.tsx";
// Define the remote modules.
const Remotes: RemoteDefinition[] = [
{ name: "remote1" }
];
const runtime = initializeFirefly(runtime, {
localModules: [registerHost, registerMyLocalModule],
remotes: Remotes,
loggers: [x => new ConsoleLogger(x)]
});
const root = createRoot(document.getElementById("root")!);
root.render(
<FireflyProvider runtime={runtime}>
<App />
</FireflyProvider>
);
#
Rename RuntimeContext.Provider
to FireflyProvider
v11.0
introduces the FireflyProvider alias for RuntimeContext.Provider
. This change is optionnal as both are still supported, but strongly encouraged.
Before:
import { initializeFirefly, RuntimeContext } from "@squide/firefly";
import { createRoot } from "react-dom/client";
const runtime = initializeFirefly();
const root = createRoot(document.getElementById("root")!);
root.render(
<RuntimeContext.Provider value={runtime}>
<App />
</RuntimeContext.Provider>
);
Now:
import { FireflyProvider, initializeFirefly } from "@squide/firefly";
import { createRoot } from "react-dom/client";
const runtime = initializeFirefly();
const root = createRoot(document.getElementById("root")!);
root.render(
<FireflyProvider runtime={runtime}>
<App />
</FireflyProvider>
);
#
Replace react-router-dom
with react-router
v10
introduces an update to React Router v7
. In React Router v7
, react-router-dom
is no longer required, as the package structure has been simplified. All necessary imports are now available from either react-router
or react-router/dom
.
#
Preparation
Before migrating to React Router v7
, it is highly recommended to read React Router migration guide and activate the "future flags" one by one to minimize breaking changes.
Before:
<RouterProvider
router={createBrowserRouter([
{
element: rootRoute,
children: registeredRoutes
}
])}
{...routerProviderProps}
/>
Now:
<RouterProvider
router={createBrowserRouter([
{
element: rootRoute,
children: registeredRoutes
}
], {
future: {
v7_relativeSplatPath: true
}
})}
{...routerProviderProps}
/>
If your application is already on React Router v7
, you can ignore this advice.
#
Update dependencies
Open a terminal at the root of the project workspace and use the following commands to remove react-router-dom
and install react-router@latest
:
pnpm remove react-router-dom
pnpm add react-router@latest
yarn remove react-router-dom
yarn add react-router@latest
npm uninstall react-router-dom
npm install react-router@latest
#
Update Imports
In your code, update all imports from react-router-dom
to react-router
, except for RouterProvider
, which must be imported from react-router/dom
.
Before:
import { Outlet, createBrowserRouter, RouterProvider } from "react-router-dom";
Now:
import { Outlet, createBrowserRouter } from "react-router";
import { RouterProvider } from "react-router/dom";
According to React Router migration guide, you can use the following command to update the imports from react-router-dom
to react-router
:
find ./path/to/src \( -name "*.tsx" -o -name "*.ts" -o -name "*.js" -o -name "*.jsx" \) -type f -exec sed -i '' 's|from "react-router-dom"|from "react-router"|g' {} +
#
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
Follow these steps to migrate an existing host application:
- Add a dependency to
@tanstack/react-query
. - Remove the
react-router-dom
dependency and update toreact-router@7*
.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 - Replace the registerLocalModules, registerRemoteModules, setMswAsReady function and the FireflyRuntime by the initializeFirefly function.
View example - Rename
RuntimeContext.Provider
for FireflyProvider.View example
#
useMsw
If the application register MSW request handlers with the runtime.registerRequestHandlers function, add the useMsw
property to the initializeFirefly function:
initializeFirefly({
useMsw: true
})
#
waitForPublicData
, waitForProtectedData
The AppRouter
component accepts the waitForPublicData
, and waitForProtectedData
properties. These properties are forwarded directly to the Squide bootstrapping flow state machine, where they are used to determine its initial state.
If the application uses the usePublicDataQueries, add the waitForPublicData
property to the AppRouter
component:
<AppRouter waitForPublicData>
...
</AppRouter>
If the application uses the useProtectedDataQueries, add the waitForProtectedData
property to the AppRouter
component:
<AppRouter waitForProtectedData>
...
</AppRouter>
Otherwise, don't define any of those three properties on the AppRouter
component.
#
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>
{({ rootRoute, registeredRoutes, routerProviderProps }) => {
return (
<RouterProvider
router={createBrowserRouter([
{
element: rootRoute,
errorElement: <RootErrorBoundary />,
children: registeredRoutes
}
])}
{...routerProviderProps}
/>
);
}}
</AppRouter>
);
}
#
Migrate a module
The changes have minimal impact on module code. To migrate an existing module, follow these steps:
- Remove the
react-router-dom
dependency and update toreact-router@7*
.View example - Convert all deferred routes into static routes.
View example - Add a
$id
option to the navigation item registrations.View example
Ensure that modules registering deferred routes are updated to convert those routes into static routes and are deployed before the host application. Failure to do so may lead to runtime errors in the production environment.
#
Isolated development
If your module is set up for isolated development, ensure that you also apply the