Developing integrations for version 2 of the Frontend SDK

Ask about this Page
Copy for LLM
View as Markdown

Custom integrations with commercetools Frontend include both backend and frontend development:

  • Backend extension API: consists of a default export with the required actions, data-sources, and dynamic-page-handler methods merged with the other required integrations.
  • Frontend SDK integration: consumes and utilizes the @commercetools/frontend-sdk package to call backend actions and handle events.

You must develop actions on the backend extension so that frontend SDK integrations can call them. For ease of development and debugging, we recommended developing the backend extension and frontend integration at the same time.

This documentation covers the development of a frontend SDK integration. For information about developing backend extensions, see Developing extensions.

Create the SDK integration

Create a folder for your SDK integration in your commercetools Frontend project inside packages/PROJECT_NAME/frontend/src/sdk. If this folder does not exist, your project might predate the addition of the commercetools Frontend SDK. In such cases, refer to the installation and setup instructions for the coFE SDK.
When developing an SDK integration, we recommend you keep the base SDK (@commercetools/frontend-sdk) package dependency up to date with the latest release. Updating the base SDK ensures you have access to the latest features and enhancements. After you complete the integration, you do not need to continuously update the dependency, however, we recommend you periodically check for new features, improvements, and bug fixes.

Implement the SDK integration

The main export of an SDK integration is a class that extends the Integration abstract class, which is imported from @commercetools/frontend-sdk.
The SDK integration must take the SDK singleton in the constructor and store the instance as a property. For example, the frontend-composable-commerce Integration class follows the same pattern. Following this structure provides consistency and ease of use, especially for SDK integrations with many methods and action type domains.

Even if you don't need to define any custom events for an SDK integration, we recommend you create and export an empty type from your integration to let you define custom events in the future. If you don't create types for your custom events, compilation errors will occur. Failure to export this type will also cause errors when adding event handlers for these events, and when adding triggers within your SDK integration.

In the following code example, we implement an SDK integration. In the sample action, the return type is Cart from the coFE domain types at packages/PROJECT_NAME/types. For complete type-safety, the commerce types should be mapped to the domain types on the backend.

SDK integration example

Basic SDK integration implementationtsx
import {
  SDK,
  Integration,
  SDKResponse,
  ServerOptions,
} from '@commercetools/frontend-sdk';
import { Cart } from '@types/cart/Cart';

type MyCustomEvents = {
  emptyCartFetched: { cartId: string };
};

type MyFirstActionPayload = {
  account: {
    email: string;
  };
};

type MyFirstActionQuery = {
  name: string;
};

type MyFirstAction = (
  payload: MyFirstActionPayload,
  query: MyFirstActionQuery,
  options: { serverOptions?: ServerOptions } = {}
) => Promise<SDKResponse<Cart>>;

class MyIntegration extends Integration<MyCustomEvents> {
  private myFirstAction: MyFirstAction = (
    payload: MyFirstActionPayload,
    query: MyFirstActionQuery,
    options: { serverOptions?: ServerOptions } = {}
  ) => {
    return this.sdk.callAction({
      actionName: 'example/myAction',
      payload,
      query,
      serverOptions: options.serverOptions,
    });
  };

  example: {
    myFirstAction: MyFirstAction;
  };

  constructor(sdk: SDK<MyCustomEvents>) {
    super(sdk);

    this.example = {
      myFirstAction: this.myFirstAction,
    };
  }
}

export { MyIntegration, MyCustomEvents };

How the SDK integration example works

In this code example:

SDK response types

In this example, the return type is Promise<SDKResponse<Cart>>. The response from the SDK can be of following types:

Implement event handling

The commercetools Frontend SDK comes with the event engine that lets integrations add event handlers and communicate with other integrations by triggering events.

Add and remove event handlers

The following code 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 eventHandler 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.
Add and remove event handlerstsx
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 sets up an event handler with a lifecycle scoped to a single React component. To avoid duplicate handlers when the component remounts, the off method is called within the cleanup function of useEffect. Without this cleanup, the handler would be added each time the component mounts, potentially causing memory leaks or unintended behavior.
For event handlers that should persist beyond a single component's lifecycle, such as those tied to the lifespan of the application, integration users can call the on method within the SDK template constructor. To attach persistent handlers directly within the SDK integration, call the on method in the SDK integration’s constructor and pass an anonymous function.

Optimize event performance

To ensure high performance, the SDK automatically skips processing for events that don't have any registered handlers. This optimization is available in SDK version 2.1.0 and later.

Prepare and trigger events only if the appropriate event handlers are listening.

Although the SDK handles internal skipping, you can use the following pattern to manually check for active handlers to avoid expensive data preparation costs in high-frequency integrations:

Only prepare and trigger event if handlers are listeningtsx
if (sdk.hasEventHandlers('productAddedToCart')) {
  const eventData = prepareEventData(product, quantity);
  sdk.trigger(
    new Event({
      eventName: 'productAddedToCart',
      data: eventData,
    })
  );
}
In this example, the hasEventHandlers method checks if any handlers are registered for the productAddedToCart event before calling the potentially expensive prepareEventData function and triggering the event.
This pattern is recommended only when the prepareEventData logic requires significant computation or heavy resource usage.

Trigger custom events

The following code 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.
Call the trigger methodtsx
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 our default strict projects, a simple truthy/falsy comparison such as !response.isError is sufficient.https://github.com/FrontasticGmbH/frontend-sdk/blob/master/src/types/sdk/SDKResponse.ts