---
title: 'Storefront: Plugin Patterns and Lifecycle | Shopware Community Hub'
description: >-
  Learn how to structure Shopware storefront plugin classes, expose stable
  methods, communicate between plugins, handle lifecycle updates, bind events
  safely,…
canonical_url: 'https://hub.shopware.com/learn/unit/storefront-plugin-patterns-and-lifecycle'
---

# Storefront: Plugin Patterns and Lifecycle

<LearningObjectives>

- Understand the difference between public methods and private helper methods in storefront plugins.
- Learn when another plugin should call a public method and when an event-based approach is cleaner.
- Understand how `init()` and `update()` behave when Shopware initializes plugins again.
- Learn how to bind event listeners safely without creating duplicate handlers.
- Understand why `this` can change inside callbacks and how to keep access to the plugin instance.
- Debug storefront plugin behavior with DOM checks, console checks, breakpoints, and plugin instance inspection.

</LearningObjectives>

# Storefront: Plugin Patterns and Lifecycle

In the previous learning unit, you learned how Shopware registers storefront plugins and turns matching DOM elements into plugin instances.

This learning unit starts one level deeper: inside the plugin class. You will learn how to structure methods, how plugins can trigger each other, how lifecycle methods behave during dynamic DOM updates, and how to bind events without creating duplicate listeners.

The goal is to make your plugins predictable. A good storefront plugin should be easy to call from the outside when needed, careful with internal helper methods, safe during re-initialization, and debuggable when something behaves differently than expected.

## Public vs. Private Methods

In storefront JavaScript plugins, methods can be either public or private, similar to PHP classes or other object-oriented programming (OOP) languages.

Private methods are internal helper methods used only inside the plugin. They are considered implementation details and may change at any time.

Public methods, on the other hand, can be called from outside the plugin.

If you come from PHP, the mental model is familiar but not identical:

- A storefront plugin class is similar to a PHP class.
- A plugin instance is similar to an object created from that class.
- Public methods are intended as stable entry points for other code or debugging.
- Private methods in Shopware storefront plugins are often a convention, commonly marked with an underscore (`_methodName`), not always enforced by the JavaScript language in the same strict way as PHP visibility keywords.

So the idea is the same as in PHP: Expose only what should be used from the outside and keep internal implementation details internal.

### Why Would a Plugin Need Public Methods?

You might ask: “Why should a method be public? My plugin runs `init()` anyway, so everything is executed.”

In real Shopware projects, a plugin is rarely completely isolated. Public methods are useful when:

- Another plugin needs to **trigger behavior** (e.g., open a panel, refresh state, re-calculate UI).
- You want a clean integration point for custom code without relying on internal helpers.
- You want to make debugging easier: you can call a public method from the console on a concrete plugin instance.

The key idea is: Public methods are not required for every plugin. In most cases, storefront plugins operate independently.

However, when external code, debugging tools, or controlled integrations need to trigger behavior on a plugin instance, a public method provides a clean and stable entry point.

In Shopware storefront plugins, a common convention is to prefix **private methods** with an underscore (`_`).

**Example:**

```js
const { PluginBaseClass } = window;

export default class ExamplePlugin extends PluginBaseClass {
    init() {
        this._registerEvents();
    }

    // Public method: Safe to call from other code
    open() {
        this.el.classList.add('is-open');
    }

    // Public method: Safe to call from other code
    close() {
        this.el.classList.remove('is-open');
    }

    // Private helper: May change anytime
    _registerEvents() {
        this.el.addEventListener('click', () => this.open());
    }
}
```

Another plugin (or custom script) can then call the public method by fetching the instance from the element:

```js
const el = document.querySelector('[data-example-plugin]');
const instance = window.PluginManager.getPluginInstanceFromElement(el, 'ExamplePlugin');

instance.open();
```

### Alternative: Communicate via `$emitter` Events

Direct method calls are not the only way to connect plugins.

In many cases, using events is the preferred approach, because it reduces direct dependencies between plugins and improves maintainability.

Instead of calling methods directly, one plugin can **publish** an event and another plugin can **subscribe** to it.

<Callout title="Shopware 6.7 Context" type="info">

This learning unit targets Shopware 6.7. The examples use the global storefront emitter APIs available in this context, such as `document.$emitter` and plugin instance emitters.

If you work in older Shopware versions, always check the storefront JavaScript APIs available in that version before copying event-based examples.

</Callout>

There are two common levels:

- **`document.$emitter`** for cross-plugin signals across the whole page.
- **`this.$emitter`** on a plugin instance for events scoped to that plugin element (`this.el`).

If you publish an event via `this.$emitter`, it is attached to the plugin element (`this.el`). It is not automatically received by `document.$emitter`.

**Example: Publish on `document.$emitter`**

```js
// Publisher (in some plugin)
// 'publish' sends an event (a signal that something happened) with a name (here: 'StickyAddToCart/visible') and some data (here: isVisible).
// Other code can react to this event and execute additional logic.
document.$emitter.publish('StickyAddToCart/visible', { isVisible: true });

// Subscriber (in another plugin)
// 'subscribe' registers your code for this event name (here: 'StickyAddToCart/visible').
// The event name must match the names used in `publish(...)`.
// Your code runs when this event is published(e.g., by another plugin or by Shopware. For example via `document.$emitter.publish('StickyAddToCart/visible', ...)`).
// The data from the event is available as `event.detail`.
document.$emitter.subscribe('StickyAddToCart/visible', (event) => {
    const { isVisible } = event.detail;
    
    // Do something with it (e.g., update the UI)
});
```

### Real Example From the Shopware Storefront

A very typical case is when one plugin triggers another plugin after a user action.

For example, Shopware’s `AddToCart` plugin updates the cart badge and (depending on configuration) opens the off-canvas cart by calling public methods on other plugin instances:

```js
const PluginManager = window.PluginManager;

// After "add to cart" succeeded:

// 1) Update header cart badge/count (CartWidget owns that UI)
PluginManager.getPluginInstances('CartWidget').forEach(function (cartWidget) {
    cartWidget.fetch();
});

// 2) Open the off-canvas cart (OffCanvasCart owns that UI)
PluginManager.getPluginInstances('OffCanvasCart').forEach(function (offCanvasCart) {
    offCanvasCart.openOffCanvas(requestUrl, formData);
});
```

In situations where a specific order of larger storefront actions is required (as shown in the example above), it is often better to split the functionality into multiple plugins. The plugins remain independent because they may also be used in other situations, but they can still form a workflow by triggering behavior through public methods.

## Lifecycle and Event Binding Patterns

Understanding the plugin lifecycle is important when attaching event listeners or reacting to dynamic DOM updates in the storefront.

### `init()` and `update()`

In Shopware storefront plugins `init()` runs only once per plugin instance. If Shopware initializes plugins again on the same element, it calls `update()`.

This is relevant for pages that update parts of the DOM (for example, listing filters, off-canvas elements, or AJAX content).

### Safe Event Binding

A common mistake is to bind new event listeners on every update. This can happen on pages where parts of the DOM are re-rendered during the same update cycle. The result can be:

- Double-click handlers.
- Duplicate AJAX requests.
- Repeated animations or UI updates.

A safe pattern is:

- Bind in `init()`.
- In `update()`, refresh DOM references or state, but do not attach the same listener again.

**Example:**

In the main.js file:

```js
import StickyAddToCartPlugin from 'src/plugin/sticky-add-to-cart/sticky-add-to-cart.plugin';

const PluginManager = window.PluginManager;

PluginManager.register('StickyAddToCart', StickyAddToCartPlugin, '[data-sticky-add-to-cart]');
```

The plugin class:

```js
const { PluginBaseClass } = window;

export default class StickyAddToCartPlugin extends PluginBaseClass {
    init() {
        // Cache a DOM reference that you will use in the scroll handler.
        this._stickyBar = this.el.querySelector('[data-sticky-add-to-cart]');

        // Attach ONE listener (do not repeat this in `update()`).
        window.addEventListener('scroll', this._onScroll.bind(this));

        // Sync UI on initial load.
        this._onScroll();
    }

    update() {
        // The DOM may have changed (AJAX, off-canvas, re-render).
        // Refresh DOM reference, but do NOT add another scroll listener.
        this._stickyBar = document.querySelector('[data-sticky-add-to-cart]');

        // Optional: re-sync UI after refresh.
        this._onScroll();
    }

    _onScroll() {
        if (null === this._stickyBar) {
            return;
        }

        const shouldShow = window.scrollY > 400;
        this._stickyBar.classList.toggle('is-visible', shouldShow);
    }
}
```

**Explanation:**

- `init()` (runs once)
  - Finds the sticky bar element (`this._stickyBar`) and stores it on the plugin instance (`this._stickyBar`).
  - Attaches one scroll listener to `window` and binds `this` so the callback can safely access `this._stickyBar`.
  - Calls `_onScroll()` once right away to sync the UI on initial load (before the user scrolls).

- `update()` (may run after DOM updates)
  - Shopware may call `update()` when parts of the DOM are replaced
  - The method refreshes the cached DOM reference (`this._stickyBar = ...`) and calls `_onScroll()` once to sync the UI again.
  - It does not attach another scroll listener (otherwise you get duplicate handlers).

- `_onScroll()`
  - Contains the actual UI logic.
  - Checks whether the sticky bar exists and toggles its visibility based on the scroll position.

### Excursus: The `this` Object

When you pass a method like `this._onScroll` to `addEventListener()` or `setTimeout()`, you are passing a **function reference**. At that moment, the method is no longer automatically connected to your plugin instance.

In JavaScript, the value of `this` depends on how the function is called, not where it is defined. For example:

- In an event listener, the browser often calls your callback with `this === element`.
- In `setTimeout()`, `this` is not your plugin instance either.

That means inside the callback function such as from `addEventListener()` or `setTimeout()`, the `this` is not reliably your plugin instance anymore, so `this.el`, `this.options`, or `this.$emitter` might not be available. In such functions, the `this` has the scope of the callback function, and it loses its connection to your plugin instance.

To fix the `this` issue, you can use `.bind(this)` so you can safely use `this` in the callback function. Calling `this._onScroll.bind(this)` creates a **new function** where `this` is permanently bound to your plugin instance. This is how you prevent losing the plugin's `this`.

As an alternative, you can use an arrow function as a wrapper. Arrow functions do not have their own `this`. They keep the `this` value from the surrounding scope (your plugin instance):

```js
window.addEventListener('scroll', () => this._onScroll());
```

<Callout title="Pitfall: Removing Listeners" type="warning">

`.bind(this)` returns a new function. If you want to later call `removeEventListener()`, store the bound function in a property first (for example `this._onScrollBound = this._onScroll.bind(this)`), and then use that same reference for `addEventListener()` and `removeEventListener()`.

</Callout>

## Common Pitfalls and Debugging

When a storefront plugin behaves unexpectedly, debug it in a fixed order. This prevents random guessing and usually helps you find the root cause quickly.

### First Checks

When debugging a storefront plugin, start with these basic checks:

- Does the element exist in the DOM?
- Is the selector correct?
- Is the plugin registered under the expected name?
- Does the element have the expected plugin instances?

By asking these questions first, you can quickly identify the most common issues during development or debugging.

### Typical Failure Scenarios

The following examples show common issues that can occur when developing storefront plugins and how to check them.

**The plugin does not run at all**

- Check that the element exists and has the expected data attribute or selector.
- Check that the plugin is registered: `window.PluginManager.getPluginList()`.
- If you injected or replaced markup (AJAX, off-canvas, CMS blocks), remember: New HTML needs a new scan via `window.PluginManager.initializePlugins()`.

**The plugin runs twice**

- Typical symptoms: Double requests, double animations, or handlers firing twice.
- Check your event binding: Bind listeners in `init()` only.
- Use `update()` for refreshing state or DOM references.
- If you see `Plugin "..." is already registered.`, you are probably registering the same plugin twice (e.g., by loading your entry bundle twice).

**Plugin options are not applied**

- Check the `data-...-options` JSON on your element.
- If the JSON is invalid, Shopware will log an error like: `The data attribute "data-...-options" could not be parsed to json: ...`.

### Where to Debug (Practical Console Checks)

Use your browser DevTools to quickly inspect the current state of the plugin system.

**Browser DevTools**

- Elements tab: Check whether the expected attributes or selectors exist.
- Console (warnings/errors): Check for errors or warnings in the console.
- Sources: Confirm that your JavaScript file is loaded successfully.

**Force a breakpoint with `debugger;`**

Insert `debugger;` into the code path you want to inspect (for example in `init()` or inside your event handler), then reload the page with DevTools open.
The browser will pause exactly there so you can inspect `this`, variables, and the call stack. Remove `debugger;` again after debugging.

**Plugin instances**

You can inspect plugin instances directly from the browser console:

```js
const el = document.querySelector('[data-buy-box]');

// 1) Does the element exist?
el;

// 2) Is the plugin registered?
window.PluginManager.getPluginList();

// 3) Does THIS element have a plugin instance?
window.PluginManager.getPluginInstanceFromElement(el, 'BuyBox');

// (Optional) Show all plugin instances attached to the element
window.PluginManager.getPluginInstancesFromElement(el);
```

## Summary

In this learning unit, you learned:

- How public methods and private helper methods keep storefront plugin classes easier to use and maintain.
- When direct method calls between plugin instances are useful, and when `$emitter` events are the cleaner option.
- How `init()` and `update()` behave when Shopware initializes plugins again after dynamic DOM changes.
- How to bind event listeners safely so repeated initialization does not create duplicate handlers.
- Why `this` can lose its plugin context inside callbacks, and how `.bind(this)` or arrow functions keep access to the plugin instance.
- How to debug plugin behavior with DOM checks, console checks, breakpoints, and plugin instance inspection.

With this knowledge, you can move from "the plugin is registered" to "the plugin behaves reliably".
