This is the early access documentation preview for Custom Views. This documentation might not be in sync with our official documentation.
Developing your integrations
Within commercetools Frontend, integrations are of two halves:
- The backend extension API, consisting of a default export with the required
actions
,data-sources
, anddynamic-page-handler
methods merged with the other required integrations. - The frontend SDK integration that consumes and utilizes the
@commercetools/frontend-sdk
package to call the backend to trigger and handle events.
Actions must be developed on the backend extension so that frontend SDK integrations can call them. For ease of development and debugging, it is recommended to develop the backend extension and frontend integrations at the same time.
This documentation covers the development of frontend SDK integration. For information on developing backend extensions, see Developing extensions.
The @commercetools/frontend-composable-commerce
SDK integration and the matching backend API are an example of a functioning integration development.
Create your SDK integration project
Create a project for your SDK in your commercetools Frontend project, then run the following commands:
yarn add @commercetools/frontend-sdk -D
yarn add @commercetools/frontend-sdk -P
yarn add @commercetools/frontend-domain-types
The SDK package must be installed as both a peerDependency
and a devDependency
. The peerDependency
is required to ensure that no version mismatch issues occur during release, causing problems with the singleton behavior of the SDK. The devDependency
is required for local development.
You can use any build tool to build and bundle your package. The following tsconfig.json
settings are recommended.
{"compilerOptions": {"target": "ES2022","strict": true,"keyofStringsOnly": true,"allowSyntheticDefaultImports": true,"types": ["node"],"declaration": true,"outDir": "lib","moduleResolution": "node"},"exclude": ["node_modules", "lib"]}
It is also recommended that your integration is bundled into one main lib/index.js
file, which will be specified in your package.json
, and that types are generated following the structure of your project. This can be achieved with some build tools or independently with tsc --emitDeclarationOnly --outDir lib
. You can also add the --watch
flag for development.
When developing your integration, it is recommended to keep both the @commercetools/frontend-sdk
peerDependency
and devDependency
up to date with the latest release. This package will always be backward compatible and therefore keeping it up to date ensures all features and developer experience improvements are accessible. Once you release the integration, it is not required to continuously update the dependency. However, it is advised to periodically check for new features and improvements as they may enhance the developer experience of your integration.
Define methods and types on your integration
The key export of an integration is a class that extends the Integration
abstract class exported from @commercetools/frontend-sdk
. Your integration must take the SDK singleton in the constructor and store the instance as a property.
An example of the above is the @commercetools/frontend-composable-commerce
Integration
class. It is recommended to follow its method structure for consistency and ease of use, especially for integrations with many methods and domains.
The Integration
abstract class also defines the CustomEvents
generic type to extend the @commercetools/frontend-sdk
StandardEvents
type, which extends the Events
type.
Even if you do not need to define any custom events for your integration, it is still recommended to create an empty type to export from your project index. This way, everything will be set to define custom events in the future. If you don't create an empty type, compilation errors will occur. Failure to export this type will cause errors to the user of the integration when trying to add event handlers for these events.
Example of integration implementation
The following is an example of integration implementation. In the sample action, the return type is Cart
from the @commercetools/frontend-domain-types
library. Commerce types will be mapped to these domain types on the backend, from whichever provider the data is fetched.
The response from the SDK will always be { isError: false, data: T }
or { isError: true, error: FetchError }
. The FetchError
type is defined in the SDK, and the generic type is defined by the type passed to the SDK’s callAction
method (sdk.callAction<T>
). In the example, the return type is defined as Promise<SDKResponse<Cart>>
.
import { SDK, Integration, SDKResponse } from '@commercetools/frontend-sdk';import { Cart } from '@commercetools/frontend-domain-types/cart/Cart';/*** Define a type for the integration's custom events, this will be exported* from the project index along with the integration. This type is used in* the generic argument of the SDK and Integration.*/type MyCustomEvents = {emptyCartFetched: { cartId: string };};/*** Define a type for the payload your action will take, this will be sent* in the body of the request.*/type MyFirstActionPayload = {account: {email: string;};};/*** Define a type for the query your action will take, this will be appended to* the URL of the request.*/type MyFirstActionQuery = {name: sring;};/*** Define a type for the action, typing this action takes advantage of the* generic nature of the SDK's callAction method and lets the user know the* return type.*/type MyFirstAction = (payload: MyFirstActionPayload,query: MyFirstActionQuery) => Promise<SDKResponse<Cart>>;/*** Define the class and extend the SDK's abstract Integration class, passing* along the MyCustomEvents type*/class MyIntegration extends Integration<MyCustomEvents> {/*** Define your action, ensuring explicit types, this will tell the user* the return type and required parameters, using the generic behavior* of the callAction method on the SDK.*/private myFirstAction: MyFirstAction = (payload: MyFirstActionPayload,query: MyFirstActionQuery) => {/*** Return the call of the SDK callAction method by passing the* action name (the path to the backend action), the payload* (can be an empty object if not needed), and the query (optional).*/return this.sdk.callAction({actionName: 'example/myAction',payload,query,});};// Define the type of the example domain object.example: {myFirstAction: MyFirstAction;};/*** Define the constructor with the SDK singleton as an argument,* passing the MyCustomEvents type again.*/constructor(sdk: SDK<MyCustomEvents>) {// Call the super method on the abstract class, passing it the SDK.super(sdk);/*** Initialize any objects with defined methods. This pattern* improves user experience for complex integrations because actions* are called in the format <integration>.<domain>.<name>.*/this.example = {myFirstAction: this.myFirstAction,};}}// Export the integration to be imported and exported in the package index.export { MyIntegration, MyCustomEvents };
The integration implementation example explained
Following is a description of the previous example.
The
MyCustomEvents
type is defined following the definition of theEvents
type in the@commercetools/frontend-sdk
package. In practice, it is recommended to define the type in another file for readability reasons, as the definition can be large.The
MyFirstActionPayload
type is defined for the action's optional payload argument. This type defines what must be passed into the integration's action call, which is serialized into the body of the request. In practice, it is recommended to define the type in another file for readability reasons, as there may be many payload types.The
MyFirstActionQuery
type is an optional query type. It must be an object type that follows the pattern[key: string]: string | number | boolean
. This query is appended to the URL of the action call. Following the example, it would be<endpoint>/frontastic/example/myAction?name=<nameValue>
. In practice, it is recommended to define the type in another file for readability reasons.The
MyFirstAction
type defines the type of the function/action itself. The parameters typed with theMyFirstActionPayload
andMyFirstActionQuery
arguments, and the return type ofPromise<SDKResponse<Cart>>
are specified. By specifying the return ofSDKResponse
with theCart
generic argument, the generic argument of the SDK'scallAction
method is used. This way, the integration user knows on successful action call they will have a return type of{ isError: false, data: Cart }
. In practice, it is recommended to define the type in another file for readability reasons.The
MyIntegration
class extends the@commercetools/frontend-sdk
Integration
abstract class, and passes theMyCustomEvents
type to the generic argument. This will allow integration users to trigger and/or add event handlers for custom events as well as the@commercetools/frontend-sdk
StandardEvents
commerce types. To interact with another integration's custom events, you must import its events and add the type to the generic argument with an intersection.The
myFirstAction
function is defined enforcing theMyFirstAction
type. The function is marked private so it cannot be called directly on the class. However, in practice, this will likely be defined elsewhere and set up externally. See the@commercetools/frontend-composable-commerce
Integration constructor for an example of how these actions can be set up.
On the return of this method, the SDK'scallAction
method is returned passing theactionName
,payload
, andquery
. TheactionName
will match the backend's default export action structure, foractionName: 'example/myAction'
the default export will call the following action on the backend.example/myAction actionName backend associationTypeScriptexport default {'dynamic-page-handler': { ... },'data-sources': { ... },actions: {example: {myAction: <function to be called>}}}The type of the
example
object is defined, where the methods for the actions in theexample
domain are also defined. Structuring the methods in this way creates a tree structure for callable methods. For example, a typical e-commerce integration might haveaccount
,cart
,product
, andwishlist
domains for actions. Therefore, by structuring the methods in this way, it is easier to find the methods to be called. An example of the domain can be found in@commercetols/frontend-composable-commerce
in theCartActions
.The
constructor
is defined with thesdk
singleton passing theMyCustomEvents
as a generic argument. This allows triggering and/or adding event handlers for custom events as well as the@commercetools/frontend-sdk
StandardEvents
commerce types. To interact with another integration's custom events, you must import its events and add the type to the generic argument with an intersection. Then, thesuper
keyword is used to invoke the constructor of the abstract base class passing it thesdk
. Finally, the example domain object is set up with thethis.myFirstAction
method definition.The
MyIntegration
andMyCustomEvents
types are exported and imported to the package's index file. From there, they are exported to be imported into your commercetools Frontend project.
For further information on structuring a large-scale integration, see the source code of the @commercetools/frontend-composable-commerce
integration.
Use the event engine
The commercetools Frontend SDK comes with event management tools. This allows integrations to communicate with other integrations and the user of the integration to add or create an event handler. The source for this functionality is the EventManager
class, extended by the SDK. It is also possible to extend the event types with custom events with the generic argument passed from the SDK.
Following is a description of the three methods available on the SDK to manage event handlers:
trigger
is called to trigger an event, for which an instance of theEvent
class from@comercetools/frontend-sdk
is passed.
An event is constructed witheventName
anddata
. TheeventName
corresponds to the[key: string]
value in@comercetools/frontend-sdk
'sStandardEvents
. In custom eventsdata
corresponds to the type of value set for the event.
For example, to trigger theemptyCartFetched
custom event defined above,trigger
is called on thesdk
and an event constructed witheventName: 'emptyCartFetched'
anddata: { cartId: "<cartId>" }
is passed.on
is called to add an event handler for an event. The method takes theeventName
andhandler
arguments.
For example, for theemptyCartFetched
custom event defined above,emptyCartFetched
is passed for theeventName
argument and a function withevent
parameter of type{ data: { cartId: "<cartId>" } }
is passed for thehandler
argument.off
is called to remove an event handler. For example, to persist the handler only for the lifecycle of a particular component.
The function takes the same arguments as theon
function. For it to work, a named function for thehandler
argument must be defined. To successfully pass a named function to thehandler
parameter, theevent
type in the function's argument must be fully typed, as shown in the following examples.
Events are likely to be triggered only during action calls. Events can be chained but it is recommended to avoid it as this can cause an infinite recursion if handled incorrectly, or if another integration interacting with yours does something similar.
Example of event triggering
Following is an example of triggering an event by calling the trigger
method.
First, the getCart
action is defined, for which a response is returned by the sdk
.
Then, the isError
parameter is checked to see if the action has errored and the trigger
method is called to trigger the standard cartFetched
event.
Finally, if the cart is empty, the trigger
method is called to trigger the emptyCartFetched
event.
getCart: async () => {const response = await sdk.callAction<Cart>({actionName: 'cart/getCart',});if (response.isError === false) {sdk.trigger(new Event({eventName: 'cartFetched',data: {cart: response.data,},}));if (!response.data.lineItems || response.data.lineItems.length === 0) {sdk.trigger(new Event({eventName: 'emptyCartFetched',data: {cartId: response.data.cartId,},}));}}return response;};
The response.isError
must be explicitly compared to the boolean value in non-strict projects for the narrowing to work on the SDKResponse union type. Otherwise, the error Property 'data' does not exist on type
will occur on when accessing response.data
. For strict projects, a simple truthy/falsy comparison such as !response.isError
is sufficient.
Example of event handler addition and removal
Following is an example of adding and removing an event handler by calling the on
and off
methods.
First, the emptyCartFetched
event handler callback is defined.
Then in the useEffect
React lifecycle hook, the on
method is called on component mounting.
Finally, a function calling the off
method on component unmounting is returned to clean up.
The emptyCartFetched
named function is defined to serve as the eventHander
parameter. The event
argument type of Event<EventName, EventData>
must be fully typed for the SDK to accept the handler argument along with "emptyCartFetched"
as the value of the eventName
parameter.
const emptyCartFetched = (event: Event<'emptyCartFetched',{cartId: string;}>) => {// Access event.data.cartId in the body of the event handler.};useEffect(() => {sdk.on('emptyCartFetched', emptyCartFetched);return () => {sdk.off('emptyCartFetched', emptyCartFetched);};}, []);
This example assumes an event handler with a lifespan of a single React component. For this reason, the off
method is called on the sdk
to clean up on unmount. Otherwise, the same handler would be added every time the component is mounted.
In practice, event handlers are likely to persist for the lifespan of the website use. In this case, integration users will call the on
method in the SDK constructor template. To add event handlers on your integration itself, call the on
method in your integration constructor and pass an anonymous function.