Skip to main content
Version: next

Components Map

Since version 3.9

What is the components map?

The components map is a way to extend the UI of an extension. It allows to register new components that will be used in the extension UI.

For instance, component map can be useful for:

  • implementing a user menu with links contributed from different extensions
  • displaying platform-specific components in the application features specific to the platform
  • customizing how polymorphic GraphQL data are displayed in an application

How to register a component map?

Registering a new component map

To register a new component map, you can use the registerFeature hook.

./example-extension/acme-extension/index.ts
export default defineExtension({
...
unstable_lifecycleHooks: {
onFeaturesInit: (hooks) => {
hooks.registerFeature("acme-feature", {
ui: {
componentsMap: {
// This should resolve to a valid file relative to the current import.meta.url
Header: new URL("./components/AcmeHeader.tsx", import.meta.url),
Toolbar: new URL("./components/AcmeToolbar.tsx", import.meta.url),
Footer: new URL("./components/AcmeFooter.tsx", import.meta.url),
},
},
});
},
},
});
Naming convention

Feature names must start with a lowercase letter. Both camelCase (e.g., "acmeFeature", "orderDetails") and dash-case (e.g., "root-error-meta") are acceptable. PascalCase names (e.g., "AcmeFeature") are not allowed.

Override an existing component map

Let's say our application needs to disable the previously registered Toolbar component which is by default registered in the acme-extension.

We can achieve this by registering a new component map with the same name, but replacing the Toolbar with null

./example-extension/application-extension/index.ts
export default defineExtension({
...
unstable_lifecycleHooks: {
onFeaturesInit: (hooks) => {
// this will extend the `acme-feature` from `acme-extension`
hooks.registerFeature("acme-feature", {
ui: {
// we only override the `Toolbar` component, the others will still resolve from `acme-extension`
componentsMap: { Toolbar: null },
},
});
},
},
});
tip

When overriding an existing component map, the last registered extension will be the components used for the feature. All the components are merged together. Not including a component, will maintain the component registered before. To unregister it, you must explicitly override it with null.

So in the above example, the final components map will be resolved as:

{
Header: AcmeHeader,
Toolbar: () => null,
Footer: AcmeFooter,
}

Co-located GraphQL fragments

Since version 3.20

Components registered in a component map can declare a co-located GraphQL fragment. This allows the component's data requirements to travel with it, so that when an extension overrides a component, the query automatically fetches the right data.

Register a component with a fragment

Instead of passing a URL directly, use the object shape with a component, a fragment, and a mandatory constraints.targetGraphQLType (or declare a feature-wide ui.constraints.targetGraphQLType — see Type constraints below):

./acme-extension/index.ts
export default defineExtension({
...
unstable_lifecycleHooks: {
onFeaturesInit: (hooks) => {
hooks.registerFeature("orderDetails", {
ui: {
constraints: { targetGraphQLType: "Order" },
componentsMap: {
OrderItems: {
component: new URL(
"./theme/modules/OrderItems/index.js",
import.meta.url,
),
fragment: /* GraphQL */ `
fragment OrderItemsFragment on Order {
items {
sku
name
qty_ordered
}
}
`,
},
},
},
});
},
},
});
warning

A constraints.targetGraphQLType is required whenever a component declares a fragment. Either declare it per-component or use a single feature-wide ui.constraints.targetGraphQLType.

At build time, Front-Commerce collects all fragments for the feature and generates a parent fragment that spreads each component's fragment. You can then use this parent fragment in your .gql files to include the data from all registered components.

Override a component with a different fragment

The fragment can be overriden in a similar manner as the component. As before, the last fragment registered will be used.

tip

The fragment name is free-form. It doesn't have to match the component key or follow any naming convention, as long as the required fields are part of the GraphQL schema. The GraphQL type declared in the fragment must match the constraint (feature-wide or per-component).

Type constraints

Constraints are required when any component in a feature declares a fragment. They ensure all fragments target the expected GraphQL type and allow Front-Commerce to generate the correct parent fragment.

Constraints can only be declared once per feature (by the first extension registering the feature). Subsequent extensions that try to redeclare constraints will trigger a build-time error.

Feature-wide constraint

Use ui.constraints.targetGraphQLType to lock all component fragments to the same type. This generates a single parent fragment named {feature}FeatureFragment:

./acme-extension/index.ts
hooks.registerFeature("orderDetails", {
ui: {
constraints: {
targetGraphQLType: "Order",
},
componentsMap: {
OrderItems: {
component: new URL("./OrderItems/index.js", import.meta.url),
fragment: /* GraphQL */ `
fragment OrderItemsFragment on Order {
items {
sku
name
}
}
`,
},
},
},
});

The generated parent fragment is:

fragment OrderDetailsFeatureFragment on Order {
...OrderItemsFragment
}

You can use ...OrderDetailsFeatureFragment in your .gql files.

Per-component constraint

Use constraints.targetGraphQLType on individual components. Each component generates its own parent fragment named {feature}_{componentKey}Fragment:

./acme-extension/index.ts
hooks.registerFeature("orderDetails", {
ui: {
componentsMap: {
OrderItems: {
component: new URL("./OrderItems/index.js", import.meta.url),
constraints: {
targetGraphQLType: "Order",
},
fragment: /* GraphQL */ `
fragment OrderItemsFragment on Order {
items {
sku
name
}
}
`,
},
},
},
});

The generated parent fragment is:

fragment OrderDetails_OrderItemsFragment on Order {
...OrderItemsFragment
}
warning

Feature-wide and per-component constraints are mutually exclusive. You cannot use both on the same feature.

Typing your features

The registerFeature function is fully typed. Feature names and their configuration are validated against the ExtensionFeatures interface from @front-commerce/types.

Declaring a new feature type

If your theme consumes a feature (through useExtensionComponentMap or useExtensionFeatureFlags), it defines the contract. Declare the expected shape in a type augmentation file:

my-theme/src/types/extension-features.d.ts
declare module "@front-commerce/types" {
export interface ExtensionFeatures {
"acme-feature": {
ui: {
componentsMap: {
Header: URL | null;
Footer: URL | null;
};
};
};
}
}

Ensure that this file is included in your tsconfig.json (usually through the include array with a src/types/**/*.d.ts pattern).

Providing or overriding a feature from another extension

If your extension registers a feature that is declared by a theme, include the theme's type declarations in your tsconfig.json so TypeScript can validate the configuration:

my-extension/tsconfig.json
{
"include": [
"../my-theme/src/types/**/*.d.ts",
"./**/*.ts",
"./**/*.tsx"
]
}

This way, TypeScript ensures that your extension passes the correct component keys and flag values for each feature.

How to use the components map?

The components map is used through the useExtensionComponentMap hook.

import { useExtensionComponentMap } from "@front-commerce/core/react";

const App = () => {
const AcmeFeature = useExtensionComponentMap("acme-feature");

return (
<div>
<AcmeFeature.Header />
<AcmeFeature.Footer />
</div>
);
};