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.
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),
},
},
});
},
},
});
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
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 },
},
});
},
},
});
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):
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
}
}
`,
},
},
},
});
},
},
});
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.
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:
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:
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
}
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:
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:
{
"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>
);
};