# 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

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.

# v9.3

Migrate to firefly v9.3

This minor version deprecate the registerLocalModules, registerRemoteModules and setMswAsReady in favor of a bootstrap function.

# v10.0

Migrate to firefly 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

Migrate to firefly 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

Migrate to firefly 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 ManagedRoutesplaceholder 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 on react-error-boundary anymore.
  • The @squide/firefly package doesn't support react-router-dom@6* anymore, remove the reacy-router-dom dependency and update to react-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 was public or protected because it wasn't registered yet. As a result, for that initial request, the route was considered protected, even if the deferred registration later registered it as public.

  • Mandatory wildcard * route registration: Previously, Squide's bootstrapping would fail if the application didn't include a wildcard route.

Before:

register.tsx
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:

register.tsx
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:

bootstrap.tsx
import { FireflyRuntime } from "@squide/firefly";

const runtime = new FireflyRuntime({
    plugins: [new MyPlugin()]
});

Now:

bootstrap.tsx
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:

bootstrap.tsx
import { initializeFirefly } from "@squide/firefly";

const runtime = initializeFirefly({
    useMsw: true
});
AppRouter.tsx
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:

bootstrap.tsx
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:

bootstrap.tsx
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

# 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, the key argument will be a concatenation of the level and index argument.

# Migrate an host application

Follow these steps to migrate an existing host application:

  1. Add a dependency to @tanstack/react-query.
  2. Remove the react-router-dom dependency and update to react-router@7*. View example
  3. Transition to the new AppRouter component. View example
  4. Create a TanStackSessionManager class and the SessionManagerContext. Replace the session's deprecated hooks by creating the customs useSession and useIsAuthenticated hooks. View example
  5. Remove the sessionAccessor option from the FireflyRuntime instance. Update the BootstrappingRoute component to create a TanStackSessionManager instance and share it down the component tree using a SessionManagedContext provider. View example
  6. Add or update the AuthenticationBoundary component to use the new useIsAuthenticated hook. Global data fetch request shouldn't be throwing 401 error anymore when the user is not authenticated. View example
  7. Update the AuthenticatedLayout component to use the session manager instance to clear the session. Retrieve the session manager instance from the context defined in the BootstrappingRoute component using the useSessionManager hook. View example
  8. Update the AuthenticatedLayout component to use the new key argument. View example
  9. Replace the ManagedRoutes placeholder with the new PublicRoutes and ProtectedRoutes placeholders. View example
  10. Convert all deferred routes into static routes. View example
  11. Add an $id option to the navigation item registrations. View example
  12. Replace the registerLocalModules, registerRemoteModules, setMswAsReady function and the FireflyRuntime by the initializeFirefly function. View example
  13. 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:

  1. Remove the react-router-dom dependency and update to react-router@7*. View example
  2. Convert all deferred routes into static routes. View example
  3. 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 host application migration steps to your isolated setup.