---
title: 'Administration Components: Behavior and Lifecycle | Shopware Community Hub'
description: >-
  Learn how to work with advanced Options API features in the Shopware
  administration, including inject and the most important lifecycle hooks.
canonical_url: >-
  https://hub.shopware.com/learn/unit/administration-components-behavior-and-lifecycle
---

# Administration Components: Behavior and Lifecycle

<LearningObjectives>

- Learn how to use advanced Options API features such as `inject`, `inheritAttrs`, and local `components`.
- Understand the **purpose and behavior** of the main Vue lifecycle hooks: `created`, `mounted`, and `updated` and when to use them.
- Differentiate between Vue Single File Components (SFCs) and Shopware components.

</LearningObjectives>

# Administration Components: Behavior and Lifecycle

In the previous learning unit, you learned the fundamental **state options** of the Vue 3 Options API as used in the Shopware administration. You now understand how data flows through a component and how the reactive state is managed.

In this learning unit, we build on that foundation. You will learn additional Option API features such as `inject`, `inheritAttrs`, and local `components` and you will learn the most important Vue lifecycle hooks.

These concepts will help you write more flexible, predictable, and extensible administration components. Let's begin!

## Options: Template Behavior

### InheritAttrs

The `inheritAttrs` option controls whether **HTML attributes** passed to a component should be **automatically added to the component's root element**.

By default, Vue automatically applies attributes like `class`, `id`, `style`, `title`, or any unknown attributes to the **root DOM element of the component**.

**Default behavior (`inheritAttrs: true`):**

Given:

```twig
<custom-input placeholder="Type here..." data-test="123" />
```

If your component looks like this:

```js
export default {
  template: `
    <div class="wrapper">
      <div class="inner">
        <input class="field" />
      </div>
    </div>
  `
  // inheritAttrs: true (By default)
}
```

Then Vue applies non-prop attributes to the root `div`, not to the `input`:

```twig
<div class="wrapper placeholder="Type here..." data-test="123">
  <div class="inner">
    <input class="field" />
  </div>
</div>
```

In many cases, this is **not** what you want for form-like components. Usually, you want `placeholder`, `disabled`, `data-*` and similar attributes to end up on the **input**, not on the wrapper.

**Using `inheritAttrs: false`**

To take full control, you can disable the automatic behavior and forward attributes manually:

```js
export default {
  template: `
    <div class="wrapper">
      <div class="inner">
        <input class="field" v-bind="$attrs" />
      </div>
    </div>
  `,
  inheritAttrs: false
}
```

With the same usage:

```twig
<custom-input placeholder="Type here..." data-test="123" />
```

The rendered HTML now looks like this:

```twig
<div class="wrapper"> {# From here #}
  <div class="inner">
    <input class="field" placeholder="Type here..." data-test="123" /> {# To here #}
  </div>
</div>
```

Now the wrapper stays clean, and all attributes are passed directly to the `input` element – exactly what you usually want in base input components.

**Why use `inheritAttrs: false`?**

- Your component has a **wrapper + inner element** structure (common in form inputs).
- You want attributes to apply to a **specific child element** instead of the wrapper.
- You want **full control** over the rendered DOM.
- You want to prevent attributes from **leaking into wrapper elements**.
- You are building **base components** or reusable UI primitives.

This pattern ensures predictable rendering and is essential for accessible inputs and complex UI widgets in the Shopware administration.

<Callout title="What does v-bind='$attrs' Do?" type="info">

The `$attrs` contains all **non-prop attributes** passed to the component (such as `placeholder`, `disabled`, `data-*`, etc.).

And `v-bind` applies these attributes to the element it is used on.

So:

```twig
<input class="field" v-bind="$attrs" />
```

**Means:** Forward every attribute the parent passed to this component onto this specific `input` element.

And together with `inheritAttrs: false`, Vue will not forward attributes automatically, and you can control where they go.

In short:

`inheritAttrs: false` = stop automatic forwarding
`v-bind="$attrs"` = manually forward attributes to the dedicated element

</Callout>

### Options: Components

Every component can register **local child components**. You import them and declare them in the `components` option.

```js
import MyComponent from './MyComponent.vue';
import MyOtherComponent from './MyOtherComponent.vue';

export default {
  components: {
    MyComponent,
    MyOtherComponent
  }
}
```

You can also **rename** the components when registering them.

```js
import MyComponent from './MyComponent.vue';
import MyOtherComponent from './MyOtherComponent.vue';

export default {
  components: {
    MyComponent,
    OrdersList: MyOtherComponent
  }
}
```

This allows you to define a clean and meaningful naming structure inside your own component.

## Options: Composition

### Inject

The `inject` option allows a component to **receive values or services from an ancestor component**. It is part of Vue's `provide/inject` mechanism and is heavily used in the Shopware administration to pass **services**, **utilities**, and **context functions**.

Inject is commonly used in Shopware to provide:

- **Global Services** (e.g., `repositoryFactory`, `loginService`, `mediaService`, `SystemConfigApiService`, and more)
- **Feature flags (`feature`)
- **ACL checks / Permission logic** (`acl`)
- **UI context functions** from parent components (tabs, trees, condition builder, etc.)

Even though these services expose backend features, they are JavaScript services inside the administration. They communicate with the backend through the Admin API, so your component can work with backend data while still being a pure frontend UI element.

**Basic Example:**

```js
export default {
  inject: ['repositoryFactory'],
  
  methods: {
    findProduct() {
      return this.repositoryFactory.create('product').search(this.criteria, Shopware.Context.api);
    }
  }
}
```

This pattern is used in many list and detail pages to load entities from the backend.

**Real-World example: Media Upload Component**

```js
export default {
  inject: ['mediaService'],
  
  data() {
    return {
      isUploading: false,
      mediaId: null,
    };
  },
  
  methods: {
    async uploadFile(file) {
      this.isUploading = true;
      
      const buffer = await file.arrayBuffer();
      const fileExtension = file.name.split('.').pop();
      
      await this.mediaService.uploadMediaById(
        this.mediaId,
        file.type,
        buffer,
        fileExtension,
        file.name
      );
      
      this.isUploading = false;
    }
  }
}
```

Then in your template:

```twig
<div class="sw-media-upload-example">
  <input type="file" @change="uploadFile" />
  
  <p v-if="isUploading">
    Uploading...
  </p>
</div>
```

**How it works:**

1. The `mediaService` is injected into the component
2. The `uploadFile` method uses the injected service to upload the file
3. The `mediaService` communicates with the backend through the Admin API
4. The component is updated with the new state

Pretty cool, right? You can now upload files in your component without having to worry about the backend.

**When to use Inject:**

- When your custom component loads a list of products with a special tag.
- When your custom component's logic depends on a plugin configuration.
- When your custom component only grants access for users with a specific role.
- When your custom component needs to load media files and display them in the template (e.g., product images).
- When your component needs utility functions from a parent component.

Of course, there are many more use cases. Inject is a powerful tool that allows you to build complex components with a clean separation of concerns.

## Options: Lifecycle

The Vue [lifecycle](https://vuejs.org/api/options-lifecycle.html) is a set of **callbacks** that are executed during different stages of the component's lifecycle.

### Created

The [created](https://vuejs.org/api/options-lifecycle.html#created) lifecycle hook runs **after the component instance has been created**, but **before** it is mounted to the DOM. This means, inside **created**, all **reactive options** such as `data`, `props`, `computed`, `methods`, `inject`, and `watchers` are already available – but the **DOM do not exist yet** (`this.$el` is still undefined).

This makes **created** the ideal place for **initializing component state** and **loading data from the backend**.

**Example: Loading a Product from the Backend**

```js
// Import Criteria from Shopware to avoid writing Shopware.Data.Criteria() multiple times
const { Criteria } = Shopware.Data;

export default {
  inject: ['repositoryFactory'],
  
  data() {
    return {
      product: null,
      criteria: null,
    };
  },
  
  created() {
    // Initialize criteria object
    this.criteria = new Criteria();
    
    this.criteria.addFilter(Criteria.equals('productId', '123456789'));
    this.criteria.addFilter(Criteria.equals('active', true));
    
    // Load an entity using injected repository
    this.loadProduct();
  },
  
  methods: {
    async loadProduct() {
      const productRepository = this.repositoryFactory.create('product');
      
      try {
        const response = await productRepository.search(this.criteria, Shopware.Context.api);
        if (response.total === 0) {
          return;
        }

        this.product = response.first();
      } catch (error) {
        // Optional: Helpful for debugging or production loggin
        // this.createNotificationError({ message: error.message }); --> Your own notification function
        // console.error(error);
      }
    }
  }
}
```

And your template could look like this:

```twig
<div class="custom-product-card">
  <p v-if="!product">Loading product...</p>
  <div v-else>
    <h3>{{ product.name }}</h3>
    <p>{{ product.description }}</p>
    <p>Product number: {{ product.productNumber }}</p>
  </div>
</div>
```

**Why is `created` so commonly used?**

The `created` option is the **default place for loading initial data**. Typical use cases include:

- Load data (list of entities or a single entity) from repositories
- Initialize component state
- Prepare criteria objects
- Fetch configuration from injected services
- Log initial state for debugging

Most built-in Shopware administration components rely heavily on `created` to load data from the backend (e.g., product details, product listing, media index, CMS detail, etc.).

### Mounted

The [mounted](https://vuejs.org/api/options-lifecycle.html#mounted) lifecycle hook runs **after the component has been inserted into the DOM**. At this point:

- The initial template has been fully rendered
- `this.$el` is available and references the root DOM element of the component
- All DOM elements inside the template can be safely accessed and manipulated

This hook is the standard place for DOM-related logic. Typical use cases include:

- Adding event listeners to DOM elements
- Initializing third-party libraries that require real DOM nodes
- Focusing input elements
- Reacting to layout changes or measuring element sizes

**Example: Automatically focusing an input element**

```js
export default {
  data() {
    return {
      searchTerm: ''
    };
  },
  mounted() {
    // Focus the input after the component has been mounted
    this.$refs.customSearchInput.focus();
  }
}
```

And in your template:

```twig
<your-custom-component>
  <input 
   ref="customSearchInput" 
   type="text"
   tabindex="-1"
  >
</your-custom-component>
```

The input element only exists **after** the component has been mounted. By assigning `ref`, you can directly reference the input element and call `focus` on it inside the mounted hook.

### Updated

The [updated](https://vuejs.org/api/options-lifecycle.html#updated) lifecycle hook runs **after the component has re-rendered**, whenever its **reactive data** (`data`, `props`, or computed dependencies) has changed. At this point:

- The DOM has been **fully patched** with the updated data
- `this.$el` reflects the **new DOM state**
- You can safely read or measure the updated DOM
- You should **avoid** trigger heavy logic or state changes inside the `updated` hook

The `updated` hook is mostly used for **DOM-related side effects that must run after a re-render**, for example:

- Recalculating layout or element sizes
- Re-applying focus after conditional UI changes
- Syncing with third-party UI libraries that depend on the updated DOM
- Observing changes visualized in the template

**Example: Scroll to the bottom when messages update**

This is a common pattern in chat-like components or activity logs.

```js
export default {
  data () {
    return {
      messages: []
    };
  },
  
  updated() {
    // After the DOM has updated, scroll to the bottom
    const box = this.$refs.messageBox;
    if (box) {
      box.scrollIntoView({ behavior: 'smooth' });
    }
  }
}
```

Template:

```twig
<div class="message-box" 
     ref="messageBox"
>
  <div class="message-item" 
       v-for="message in messages" :key="message.id"
  >
    {{ message.text }}
  </div>
</div>
```

Whenever `messages` changes, Vue re-renders the list, the `updated` hook runs, and the component scrolls to the bottom.

<Callout title="Difference Between mounted and updated" type="info">

`mounted` runes **only once**, after the initial render. Use `mounted` for **one-time DOM initialization**.
`updated` runes **every time** the component re-renders due to reactive changes. Use `updated` for **DOM work that must happen after data changes** and after Vue updates the DOM

</Callout>

## Vue Single File Component (SFC) vs. Shopware Component

When working with the Shopware administration, it is important to understand that **you are not writing traditional Vue Single File Components (SFCs)**.

Shopware uses a **Vue-like component system**, but with its own conventions and build pipeline. Shopware components differ from classic `.vue` files in several ways.

### Vue Single File Component (SFC)

A standard Vue SFC usually looks like this:

```vue
<template>
  <div>{{ message }}</div>
</template>

<script>
  export default {
    data() {
      return {
        message: 'Hello!'
      };
    }
  }
</script>

<style scoped>
  div {
    color: green;
  }
</style>
```

SFCs **combine** the template, script, and style into a **single file**.

### Shopware Component

In Shopware, components are **split across multiple files** and registered globally through the Shopware component registry.

A typical Shopware component looks like this:

```text
└── [shop_root]
     └── custom
         └── plugins
             └── [your_plugin]
                  └── src
                      └── Resources
                          └── app
                              └── administration
                                  └── src
                                      ├── module
                                      │   └── swag-example
                                      │       └── component
                                      │             └── my-component
                                      │                 └── my-component.html.twig --> Template
                                      │                 └── my-component.scss --> Styles
                                      │                 └── my-component.js --> Script / Options API
```

Shopware uses a separation of concerns to **keep components small and focused**. In the last step, you **register** your component via `Shopware.Component.register()`.

| Concept              | Vue SFC (.vue)                     | Shopware Component                             |
|----------------------|------------------------------------|------------------------------------------------|
| File format          | `.vue` single files                | Separate `.html.twig`, `.js`, `.scss`          |
| Registration         | Local or auto-import via ES module | Via `Shopware.Component.register`              |
| Template             | Pure Vue HTML                      | **Twig.js** (Only Twig blocks, Vue compatible) |
| Component discovery  | Local/import-based                 | **Global registry**                            |

This concept makes it easier to **manage and reuse components** across the administration and makes it more clean, readable, maintainable, and extendable.

So as a mental model: A Shopware component is a Vue component, but structured the Shopware way.

## Summary

In this learning unit, you learned several advanced parts of the Vue 3 Options API as they are used in the Shopware administration.

By now, you should be able to:

- Control attribute forwarding using `inheritAttrs` and manually forwarding attributes with `$attrs`.
- Register and structure local components through the `components` option.
- Use `inject` to access Shopware services, feature flags, ACL checks, and shared context function.
- Apply lifecycle hooks (`created`, `mounted`, `updated`) to initialize data, work with the DOM, and react to re-renders.
- Understand the difference between Vue Single File Components (SFCs) and Shopware components and why Shopware uses a different structure.

Well done! With this understanding, you are ready to build more maintainable, predictable, and modular administration components.
