# Override a React context

In a modular application, it's typical to configure various global React context at the root of the host application. These contexts are then used by the layouts and pages of the modules.

Let's explore a simple example using a BackgroundColorContext:

host/src/App.tsx
import { AppRouter } from "@squide/firefly";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { BackgroundColorContext } from "@sample/shared";

export function App() {
    return (
        <BackgroundColorContext.Provider value="blue">
            <AppRouter waitForMsw={false}>
                {({ rootRoute, registeredRoutes, routerProviderProps }) => {
                    return (
                        <RouterProvider
                            router={createBrowserRouter([
                                {
                                    element: rootRoute,
                                    children: registeredRoutes
                                }
                            ])}
                            {...routerProviderProps}
                        />
                    );
                }}
            </AppRouter>
        </BackgroundColorContext.Provider>
    );
}
remote-module/src/register.tsx
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { ColoredPage } from "./ColoredPage.tsx";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        path: "/colored-page",
        element: <ColoredPage />
    });
}
remote-module/src/ColoredPage.tsx
import { useBackgroundColor } from "@sample/shared";

export function ColoredPage() {
    const backgroundColor = useBackgroundColor();

    return (
        <div style={{ backgroundColor }}>
            The background color is "{backgroundColor}"
        </div>
    );
}

In the previous code samples, the host application provides a value for the BackgroundColorContext, and the ColoredPage component of the remote module uses this value to set its background color (to blue for this example).

# Override the context for the module

Now, suppose the requirements change, and a page of the remote module must have a red background. The context can be overriden for that page by declaring a new provider directly in the routes registration:

remote-module/src/register.tsx
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { BackgroundColorContext } from "@sample/shared";
import { ColoredPage } from "./ColoredPage.tsx";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        path: "/colored-page",
        element: (
            <BackgroundColorContext.Provider value="red">
                <ColoredPage />
            </BackgroundColorContext.Provider>
        )
    });
}

# Extract an utility component

Since there are multiple routes to setup with the new provider, an utility component can be extracted:

remote-module/src/register.tsx
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { BackgroundColorContext } from "@sample/shared";
import { ColoredPage } from "./ColoredPage.tsx";
import type { ReactNode } from "react";

function RedBackground({ children }: { children: ReactNode }) {
    return (
        <BackgroundColorContext.Provider value="red">
            {children}
        </BackgroundColorContext.Provider>
    );
}

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        path: "/colored-page",
        element: <RedBackground><ColoredPage /></RedBackground>
    });
}

# Update a singleton dependency version

Let's consider a more specific use case where the host application declares a ThemeContext from Workleap's new design system, Hopper:

host/src/App.tsx
import { AppRouter } from "@squide/firefly";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { ThemeContext } from "@hopper/components";

export function App() {
    return (
        <ThemeContext.Provider value="dark">
            <AppRouter waitForMsw={false}>
                {({ rootRoute, registeredRoutes, routerProviderProps }) => {
                    return (
                        <RouterProvider
                            router={createBrowserRouter([
                                {
                                    element: rootRoute,
                                    children: registeredRoutes
                                }
                            ])}
                            {...routerProviderProps}
                        />
                    );
                }}
            </AppRouter>
        </ThemeContext.Provider>
    );
}

In this scenario, Hopper's components are used throughout the entire application, including the modules. Moreover, @hopper/components is defined as a singleton shared dependency:

host/webpack.dev.js
// @ts-check

import { defineDevHostConfig } from "@squide/firefly-webpack-configs";
import { swcConfig } from "./swc.dev.js";

export default defineDevHostConfig(swcConfig, 8080, [...], {
    sharedDependencies: {
        "@hopper/components": {
            singleton: true
        }
    }
});
remote-module/webpack.dev.js
// @ts-check

import { defineDevRemoteModuleConfig } from "@squide/firefly-webpack-configs";
import { swcConfig } from "./swc.dev.js";

export default defineDevRemoteModuleConfig(swcConfig, "remote-module", 8080, {
    sharedDependencies: {
        "@hopper/components": {
            singleton: true
        }
    }
});

Now, consider a situation where Hopper releases a new version of the package that includes breaking changes, without a "compatibility" package to ensure backward compatility with the previous version.

To update the host application without breaking the modules, we recommend to temporary "break" the singleton shared dependency by loading two versions of the @hopper/components dependency in parallel (one for the host application and one for the modules that have not been updated yet):

remote-module/webpack.dev.js
// @ts-check

import { defineDevRemoteModuleConfig } from "@squide/firefly-webpack-configs";
import { swcConfig } from "./swc.dev.js";

export default defineDevRemoteModuleConfig(swcConfig, "remote-module", 8080, {
    sharedDependencies: {
        "@hopper/components": {
            singleton: false
        }
    }
});

As @hopper/components expose the ThemeContext, the context must be re-declared in each module until every part of the federated application has been updated to the latest version of @hopper/components:

remote-module/src/register.tsx
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { ThemeContext } from "@hopper/components";
import { Page } from "./Page.tsx";
import type { ReactNode } from "react";

function Providers({ children }: { children: ReactNode }) {
    return (
        <ThemeContext.Provider value="dark">
            {children}
        </ThemeContext.Provider>
    )
}

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        path: "/page",
        element: <Providers><Page /></Providers>
    });
}

Thankfully, React Router makes it very easy to declare contexts in a module.