Skip to main content

Understanding the App Shell

Your application shell treats ProductConfigurator as the single source of product state and the use* hooks as the public control surface around it. This guide shows how to structure a host app: share one instance, watch the lifecycle, render loading/ready/error states, and create feature controllers at the right time. The snippets follow the real Vue integration in apps/options-vue.

Share one configurator instance

Create one ProductConfigurator per configurator session and make it available to the whole component tree. In the Vue app, the entry point receives the instance and provides it via Vue's dependency injection:

// main.ts
export function ExpiviProductComposer(
aContainer: HTMLElement | string,
aProductConfigurator: ProductConfigurator,
aOptions: ExpiviProductComposerOptions = {},
) {
const app = createApp(App);
app.provide(productConfiguratorKey, aProductConfigurator);
app.mount(aContainer);
return app;
}

How you share it depends on your framework — provide/inject in Vue, context in React, or a shared module in a custom setup. The important part is that every component creates its controllers from the same instance.

Watch the lifecycle with useProductConfigurator

The root shell creates useProductConfigurator first. Unlike the other hooks, it is safe to create before the product is ready, and its subscribeStatus callback lets you react to each lifecycle transition:

const configurator = inject<ProductConfigurator>(productConfiguratorKey);
const controller = useProductConfigurator(configurator);

controller.subscribeStatus((status: ProductConfiguratorStatus) => {
if (status === ProductConfiguratorStatus.READY) {
// product is fully initialized — enable feature areas
}

if (status === ProductConfiguratorStatus.ERROR) {
// loading failed — show an error state
}
});

You can also read the current value at any time via controller.status. subscribeStatus returns an unsubscribe function — call it when the shell unmounts.

Render loading, ready, and error states

Because the product loads asynchronously (the shell mounts while loadProduct is still running), your root component should render at least three states off the status:

<template>
<div class="xpv-root__app">
<xpv-loader v-if="!loaded && !failed" />

<MainProductOffer v-else-if="!failed" />

<div v-else>Failed to load the Product.</div>
</div>
</template>
const loaded = ref(false);
const failed = ref(false);

controller.subscribeStatus((status) => {
if (status === ProductConfiguratorStatus.READY) loaded.value = true;
if (status === ProductConfiguratorStatus.ERROR) failed.value = true;
});

This is the simplest safe shell for an Expivi integration: a loader until READY, the product UI once ready, and a fallback on ERROR.

Create feature controllers after READY

Hooks built on the store (everything except useProductConfigurator) must not be created until the configurator is at least LOADED — they throw Product configurator is not initialized. otherwise. The clean pattern is to instantiate them inside the READY branch of subscribeStatus:

const messageController = ref<MessagesController | undefined>(undefined);

controller.subscribeStatus((status) => {
if (status === ProductConfiguratorStatus.READY) {
messageController.value = useMessages(configurator);
}
});

The same rule applies to the other feature hooks you build against the ready configurator, for example usePriceBreakdown, useVisualization2d, useBomBreakdown, useAdjacentProducts, and useProductSelection. Create each one in the feature component that owns it, from the shared configurator instance.

Canvas editor overlay (Web2Print)

If your product uses Web2Print canvas attributes, the shell usually owns the editor overlay. The key constraint is that the editor's DOM target must exist before the editor is initialized, so you show the container first and initialize on the next tick:

import { createCanvasEditor } from "@expivi/canvas-editor-core";

let canvasApp: ReturnType<typeof createCanvasEditor> | null = null;

const openCanvas = (controller?: Web2PrintCanvasAttributeController) => {
showCanvas.value = true; // renders <div id="canvas-host">
// wait for the DOM update before mounting the editor into the target
setTimeout(() => {
canvasApp = createCanvasEditor("#canvas-host", { controller });
}, 0);
};

const closeCanvas = () => {
canvasApp?.unmount();
canvasApp = null;
showCanvas.value = false;
};

createCanvasEditor takes a Web2PrintCanvasAttributeController (obtained from the attribute the user is editing) and mounts a full canvas editor into the target element. Open and close it from a central place in the shell so only one editor is ever mounted.

  • Keep one shared ProductConfigurator instance per mounted configurator.
  • Let the root shell own lifecycle state (loading, ready, failure) via useProductConfigurator.
  • Create store-backed controllers only after READY, in the feature components that use them.
  • Treat the use* hooks as the supported integration layer over the underlying store.