diff --git a/app/controllers/my_courses.php b/app/controllers/my_courses.php
index 8205c12c743e0a1855bd7473d6b6a2f9ee14ced4..c5c10dd27d198f21e659565878c1a42b6b077cb9 100644
--- a/app/controllers/my_courses.php
+++ b/app/controllers/my_courses.php
@@ -120,6 +120,12 @@ class MyCoursesController extends AuthenticatedController
             ['type' => 'text/javascript'],
             'window.STUDIP.MyCoursesData = ' . json_encode($data) . ';'
         );
+
+        $this->render_vue_app(
+            Studip\VueApp::create('MyCourses')
+                ->withStore('MyCoursesStore')
+                ->withStoreData('mycourses', $data)
+        );
     }
 
     /**
diff --git a/lib/classes/StudipController.php b/lib/classes/StudipController.php
index a908a477c376cbfa6cb2fa319ed2d8bdcc081b6e..ad6a481129186c0815b5ca641c6a05abdcb14991 100644
--- a/lib/classes/StudipController.php
+++ b/lib/classes/StudipController.php
@@ -588,6 +588,45 @@ abstract class StudipController extends Trails\Controller
         $this->render_text($form->render());
     }
 
+    public function render_vue_app(\Studip\VueApp $app): void
+    {
+        $attributes = [
+            'data-vue-app' => json_encode([
+                'components' => [$app->getBaseComponent()],
+                'store'      => $app->getStore(),
+            ]),
+            'is' => $app->getBaseComponent(),
+
+            ...$app->getProps(),
+        ];
+
+        $content = '';
+
+        if ($app->getStoreData()) {
+            $content .= '<script>';
+            foreach ($app->getStoreData() as $key => $data) {
+                $content .= sprintf(
+                    "window.STUDIP.Vue.setStoreData('%s', %s);",
+                    $key,
+                    json_encode($data),
+                );
+            }
+            $content .= '</script>';
+        }
+
+        $content .= '<div ' . arrayToHtmlAttributes($attributes) . '></div>';
+
+
+        if ($this->layout) {
+            $content = $this->get_template_factory()->render(
+                $this->layout,
+                ['content_for_layout' => $content]
+            );
+        }
+
+        $this->render_text($content);
+    }
+
     /**
      * relays current request to another controller and returns the response
      * the other controller is given all assigned properties, additional parameters are passed
diff --git a/lib/classes/VueApp.php b/lib/classes/VueApp.php
new file mode 100644
index 0000000000000000000000000000000000000000..6c40c9e9763ff5bfbafcffc202b7dbe97b7df0bb
--- /dev/null
+++ b/lib/classes/VueApp.php
@@ -0,0 +1,63 @@
+<?php
+namespace Studip;
+
+class VueApp
+{
+    public static function create(string $base_component, array $props = []): VueApp
+    {
+        return new self($base_component, $props);
+    }
+
+    protected ?string $store = null;
+    protected array $storeData = [];
+
+    public function __construct(
+        protected string $base_component,
+        protected array $props = []
+    ) {
+    }
+
+    public function withBaseComponent(string $base_component): VueApp
+    {
+        $this->base_component = $base_component;
+        return $this;
+    }
+
+    public function getBaseComponent(): string
+    {
+        return $this->base_component;
+    }
+
+    public function withProps(array $props): VueApp
+    {
+        $this->props = $props;
+        return $this;
+    }
+
+    public function getProps(): array
+    {
+        return $this->props;
+    }
+
+    public function withStore(?string $store): VueApp
+    {
+        $this->store = $store;
+        return $this;
+    }
+
+    public function getStore(): ?string
+    {
+        return $this->store;
+    }
+
+    public function withStoreData(string $key, array $data): VueApp
+    {
+        $this->storeData[$key] = $data;
+        return $this;
+    }
+
+    public function getStoreData(): array
+    {
+        return $this->storeData;
+    }
+}
diff --git a/resources/assets/javascripts/bootstrap/vue.js b/resources/assets/javascripts/bootstrap/vue.js
index b8c938d2904a4dbf8a0d2dd3829fb699614dc4c9..b3341d06da05f5473fffc36be79d360d61cd1ddc 100644
--- a/resources/assets/javascripts/bootstrap/vue.js
+++ b/resources/assets/javascripts/bootstrap/vue.js
@@ -33,7 +33,7 @@ STUDIP.ready(() => {
                 const storeConfig = await import(`../../../vue/store/${config.store}.js`);
                 console.log('store', storeConfig.default);
 
-                store.registerModule(config.id, storeConfig.default, {root: true});
+                store.registerModule(config.id, storeConfig.default);
 
                 Object.keys(data).forEach(command => {
                     store.commit(`${config.id}/${command}`, data[command]);
diff --git a/resources/assets/javascripts/lib/studip-vue.js b/resources/assets/javascripts/lib/studip-vue.js
index c7cf89a2924001d0b2fe6042a7e911a540bd7cbd..395add9d1418f4dba0f285da4cae63c43b0a170c 100644
--- a/resources/assets/javascripts/lib/studip-vue.js
+++ b/resources/assets/javascripts/lib/studip-vue.js
@@ -1,15 +1,33 @@
-const load = async function () {
-    return STUDIP.loadChunk('vue');
-};
+class Vue
+{
+    static #storeData = {};
 
-const on = async function (...args) {
-    const { eventBus } = await load();
-    eventBus.on(...args);
-};
+    static async load()
+    {
+        return STUDIP.loadChunk('vue');
+    }
 
-const emit = async function (...args) {
-    const { eventBus } = await load();
-    eventBus.emit(...args);
-};
+    static async on(...args)
+    {
+        const { eventBus } = await this.load();
+        eventBus.on(...args);
+    }
 
-export default { load, on, emit };
+    static async emit(...args)
+    {
+        const { eventBus } = await this.load();
+        eventBus.emit(...args);
+    }
+
+    static setStoreData(key, data)
+    {
+        this.#storeData[key] = data;
+    }
+
+    static getStoreData(key)
+    {
+        return this.#storeData[key] ?? null;
+    }
+}
+
+export default Vue;