Von Katharina „Kat“ Schmidt & Nova Trent | Java Fleet Systems Consulting
Kat: Frontend-Spezialistin & Mentorin 🎨
Nova: Junior Developer & Lernende 🌟
📚 Was bisher geschah – Vue.js für Anfänger
Bereits veröffentlicht:
- ✅ Teil 1: Setup & Erste API-Anbindung
- ✅ Teil 2: Styling & Responsive Design
- ✅ Teil 3: Formulare & User Input
- ✅ Teil 4: Listen-Manipulation & Reaktivität
- ✅ Teil 5: CRUD Operations Complete
- ✅ Teil 6: Component Communication (Props & Emits)
Heute: Teil 7 fokussiert auf State Management – Composables & Pinia!
Neu in der Serie? Teil 6 (Props & Emits) ist Voraussetzung für heute!
⚡ Kurze Zusammenfassung – Das Wichtigste in 30 Sekunden
Dein Problem: Dein Code wiederholt sich! Mehrere Components brauchen dieselbe Logik. Props-Drilling nervt. Wo speicherst du globalen State? 🤯
Die Lösung: Composables für wiederverwendbare Logik, Pinia für globalen State!
Heute lernst du:
- ✅ Composables erstellen (eigene „Hooks“!)
- ✅ Logik aus Components extrahieren
- ✅ Pinia Setup & Stores
- ✅ State, Getters, Actions
- ✅ Wann Composables vs Pinia?
- ✅ Best Practices für State Management
Dein größter Gewinn: DRY-Code (Don’t Repeat Yourself) und saubere Architektur! 🏗️
Zeit-Investment: 75-90 Minuten | Schwierigkeit: Mittel-Fortgeschritten
👋 Nova: „Ich kopiere denselben Code überall!“
Hi Leute! Nova hier – und ich hab ein Déjà-vu! 😵💫
Letzte Woche haben wir Props & Emits gelernt. Meine App ist jetzt modular.
ABER…
Ich hab gerade gemerkt: Ich kopiere denselben Code in JEDE Component!
<!-- TaskList.vue -->
<script setup>
const tasks = ref([]);
const isLoading = ref(false);
async function fetchTasks() {
isLoading.value = true;
const response = await fetch('/api/tasks');
tasks.value = await response.json();
isLoading.value = false;
}
onMounted(() => fetchTasks());
</script>
<!-- TaskStats.vue - DERSELBE CODE! 😱 -->
<script setup>
const tasks = ref([]);
const isLoading = ref(false);
async function fetchTasks() {
isLoading.value = true;
const response = await fetch('/api/tasks');
tasks.value = await response.json();
isLoading.value = false;
}
onMounted(() => fetchTasks());
</script>
<!-- Dashboard.vue - SCHON WIEDER! 😭 --> <script setup> const tasks = ref([]); // ... derselbe Code zum dritten Mal! </script>
Nova: „KAT! Ich kopiere denselben Fetch-Code in jede Component! Das kann doch nicht richtig sein!“
Kat: [schaut auf den Code] „Oh ja, das ist ein klassisches Problem. Du brauchst Composables!“
Nova: „Compo… was?!“
Kat: „Composables! Wie React Hooks, aber für Vue. Wiederverwendbare Logik!“
Nova: „Und was ist mit diesem ‚Pinia‘, von dem alle reden?“
Kat: „Das kommt auch! Aber eins nach dem anderen. Setz dich!“ 🎓
🎣 Kat: Was sind Composables?
Hey! Kat hier! 👋
Composables sind Funktionen, die Vue’s Composition API nutzen und wiederverwendbare Logik kapseln.
Das Konzept
// composables/useTasks.js
import { ref, onMounted } from 'vue';
export function useTasks() {
const tasks = ref([]);
const isLoading = ref(false);
const error = ref(null);
async function fetchTasks() {
isLoading.value = true;
error.value = null;
try {
const response = await fetch('/api/tasks');
tasks.value = await response.json();
} catch (e) {
error.value = e.message;
} finally {
isLoading.value = false;
}
}
onMounted(() => fetchTasks());
return {
tasks,
isLoading,
error,
fetchTasks
};
}
Verwendung in Components
<!-- TaskList.vue - CLEAN! -->
<script setup>
import { useTasks } from '@/composables/useTasks';
const { tasks, isLoading, error } = useTasks();
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<TaskCard v-for="task in tasks" :key="task.id" :task="task" />
</template>
<!-- TaskStats.vue - AUCH CLEAN! -->
<script setup>
import { useTasks } from '@/composables/useTasks';
const { tasks } = useTasks();
const stats = computed(() => ({
total: tasks.value.length,
completed: tasks.value.filter(t => t.completed).length
}));
</script>
Nova: „OH! Das ist wie eine Funktion die ich überall importieren kann!“
Kat: „Genau! Aber mit reaktiven Daten. Wie ein ‚Custom Hook‘ in React!“
🛠️ Praxis: Dein erstes Composable
Projekt-Struktur
src/ ├── composables/ ← NEUER Ordner! │ ├── useTasks.js ← Task-Logik │ ├── useLocalStorage.js ← LocalStorage-Wrapper │ └── useToast.js ← Toast-Notifications ├── components/ └── App.vue
useLocalStorage.js – Praktisches Beispiel
// composables/useLocalStorage.js
import { ref, watch } from 'vue';
export function useLocalStorage(key, defaultValue) {
// Initial: Aus localStorage laden oder Default
const stored = localStorage.getItem(key);
const data = ref(stored ? JSON.parse(stored) : defaultValue);
// Bei Änderung: In localStorage speichern
watch(data, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
}, { deep: true });
return data;
}
Verwendung:
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage';
// Automatisches Speichern & Laden!
const tasks = useLocalStorage('my-tasks', []);
const settings = useLocalStorage('user-settings', { theme: 'light' });
// Einfach nutzen wie normales ref()
tasks.value.push({ id: 1, title: 'Neu!' });
// → Wird automatisch in localStorage gespeichert!
</script>
Nova: „Wait… das speichert AUTOMATISCH?!“
Kat: „Ja! watch() beobachtet Änderungen und speichert. Magic! ✨“
useToast.js – Toast-System
// composables/useToast.js
import { ref } from 'vue';
const toasts = ref([]);
let toastId = 0;
export function useToast() {
function show(message, type = 'success', duration = 3000) {
const id = toastId++;
toasts.value.push({ id, message, type });
setTimeout(() => {
const index = toasts.value.findIndex(t => t.id === id);
if (index !== -1) toasts.value.splice(index, 1);
}, duration);
}
return {
toasts,
success: (msg) => show(msg, 'success'),
error: (msg) => show(msg, 'error'),
warning: (msg) => show(msg, 'warning'),
info: (msg) => show(msg, 'info')
};
}
Verwendung:
<script setup>
import { useToast } from '@/composables/useToast';
const toast = useToast();
function handleSave() {
// ... save logic
toast.success('Gespeichert! ✅');
}
function handleError() {
toast.error('Etwas ist schiefgelaufen! ❌');
}
</script>
🍍 Pinia: Globaler State Management
Das Problem mit Composables
Nova: „Warte… wenn ich useTasks() in zwei Components aufrufe, haben die dann DENSELBEN State?“
Kat: „Gute Frage! Nein – jeder Aufruf erstellt NEUE refs!“
<!-- Component A -->
<script setup>
const { tasks } = useTasks(); // tasks = []
tasks.value.push({ id: 1 }); // tasks = [{ id: 1 }]
</script>
<!-- Component B -->
<script setup>
const { tasks } = useTasks(); // tasks = [] ← NICHT synchronisiert!
</script>
Für GETEILTEN State brauchst du: Pinia!
Was ist Pinia?
Pinia ist der offizielle State Manager für Vue.js:
- 🍍 Leichtgewichtig & modern
- 🔧 DevTools Integration
- 📦 Modular (mehrere Stores)
- 🎯 TypeScript-freundlich
Pinia installieren
npm install pinia
Pinia Setup
// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount('#app');
Dein erster Store
// stores/taskStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useTaskStore = defineStore('tasks', () => {
// ========================================
// STATE
// ========================================
const tasks = ref([]);
const isLoading = ref(false);
const error = ref(null);
const filter = ref('all'); // 'all', 'open', 'completed'
// ========================================
// GETTERS (Computed)
// ========================================
const filteredTasks = computed(() => {
if (filter.value === 'open') {
return tasks.value.filter(t => !t.completed);
}
if (filter.value === 'completed') {
return tasks.value.filter(t => t.completed);
}
return tasks.value;
});
const stats = computed(() => ({
total: tasks.value.length,
completed: tasks.value.filter(t => t.completed).length,
open: tasks.value.filter(t => !t.completed).length
}));
const progress = computed(() => {
if (tasks.value.length === 0) return 0;
return Math.round((stats.value.completed / stats.value.total) * 100);
});
// ========================================
// ACTIONS
// ========================================
async function fetchTasks() {
isLoading.value = true;
error.value = null;
try {
// Simuliert API-Call (oder echte API)
const saved = localStorage.getItem('pinia-tasks');
if (saved) {
tasks.value = JSON.parse(saved);
}
} catch (e) {
error.value = e.message;
} finally {
isLoading.value = false;
}
}
function addTask(taskData) {
const newTask = {
id: Date.now(),
...taskData,
completed: false,
createdAt: new Date().toISOString()
};
tasks.value.push(newTask);
saveToStorage();
}
function updateTask(id, updates) {
const index = tasks.value.findIndex(t => t.id === id);
if (index !== -1) {
tasks.value[index] = { ...tasks.value[index], ...updates };
saveToStorage();
}
}
function deleteTask(id) {
tasks.value = tasks.value.filter(t => t.id !== id);
saveToStorage();
}
function toggleTask(id) {
const task = tasks.value.find(t => t.id === id);
if (task) {
task.completed = !task.completed;
saveToStorage();
}
}
function setFilter(newFilter) {
filter.value = newFilter;
}
// Helper
function saveToStorage() {
localStorage.setItem('pinia-tasks', JSON.stringify(tasks.value));
}
// ========================================
// RETURN - Was Components nutzen können
// ========================================
return {
// State
tasks,
isLoading,
error,
filter,
// Getters
filteredTasks,
stats,
progress,
// Actions
fetchTasks,
addTask,
updateTask,
deleteTask,
toggleTask,
setFilter
};
});
Store verwenden
<!-- TaskList.vue -->
<template>
<div>
<div v-if="taskStore.isLoading">Loading...</div>
<div v-else-if="taskStore.error">Error!</div>
<TaskCard
v-for="task in taskStore.filteredTasks"
:key="task.id"
:task="task"
@toggle="taskStore.toggleTask(task.id)"
@delete="taskStore.deleteTask(task.id)"
/>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
import { useTaskStore } from '@/stores/taskStore';
const taskStore = useTaskStore();
onMounted(() => {
taskStore.fetchTasks();
});
</script>
<!-- TaskStats.vue - DERSELBE STORE! -->
<template>
<div class="stats">
<div>Total: {{ taskStore.stats.total }}</div>
<div>Erledigt: {{ taskStore.stats.completed }}</div>
<div>Fortschritt: {{ taskStore.progress }}%</div>
</div>
</template>
<script setup>
import { useTaskStore } from '@/stores/taskStore';
const taskStore = useTaskStore();
// Kein fetchTasks() nötig - Store ist bereits gefüllt!
</script>
Nova: „OH! Beide Components nutzen DENSELBEN State?!“
Kat: „Exactly! Ein Store, überall synchronisiert! Das ist die Power von Pinia!“
🤔 Composables vs Pinia – Wann was?
Entscheidungs-Matrix
| Situation | Lösung | Warum? |
|---|---|---|
| Wiederverwendbare Logik (nicht State) | Composable | z.B. useDebounce, useEventListener |
| Lokaler Component-State | ref() | Nur eine Component braucht es |
| Geteilter State zwischen WENIGEN Components | Composable mit Singleton | z.B. useToast (global) |
| Globaler App-State | Pinia Store | Tasks, User, Cart, etc. |
| Server-State (API-Daten) | Pinia + Composable | Store für State, Composable für Fetch-Logik |
Beispiele
✅ Composable (Logik ohne geteilten State):
// useDebounce.js - Reines Utility
export function useDebounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
✅ Composable mit Singleton (Geteilter State):
// useAuth.js - Singleton Pattern
const user = ref(null);
const isAuthenticated = computed(() => !!user.value);
export function useAuth() {
// Alle Components teilen dieselben refs!
return { user, isAuthenticated, login, logout };
}
✅ Pinia (Komplexer globaler State):
// stores/cartStore.js
export const useCartStore = defineStore('cart', () => {
const items = ref([]);
const total = computed(() => /* ... */);
// Viele Actions, komplexe Logik...
});
🏗️ Praxis: Task-Manager mit Pinia
Projekt-Struktur
src/ ├── stores/ │ └── taskStore.js ← Pinia Store ├── composables/ │ ├── useLocalStorage.js ← Utility │ └── useToast.js ← Toast-System ├── components/ │ ├── TaskForm.vue │ ├── TaskFilters.vue │ ├── TaskStats.vue │ ├── TaskList.vue │ ├── TaskCard.vue │ └── ToastContainer.vue ├── App.vue └── main.js
taskStore.js (vollständig)
// stores/taskStore.js
import { defineStore } from 'pinia';
import { ref, computed, watch } from 'vue';
export const useTaskStore = defineStore('tasks', () => {
// ========================================
// STATE
// ========================================
const tasks = ref([]);
const isLoading = ref(false);
const error = ref(null);
const currentFilter = ref('all');
const searchQuery = ref('');
// ========================================
// PERSISTENCE (Auto-Save)
// ========================================
const STORAGE_KEY = 'vue-tasks-pinia';
function loadFromStorage() {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
try {
tasks.value = JSON.parse(saved);
} catch (e) {
console.error('Failed to load tasks:', e);
}
}
}
function saveToStorage() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks.value));
}
// Watch for changes and auto-save
watch(tasks, saveToStorage, { deep: true });
// ========================================
// GETTERS
// ========================================
const filteredTasks = computed(() => {
let result = tasks.value;
// Filter by status
if (currentFilter.value === 'open') {
result = result.filter(t => !t.completed);
} else if (currentFilter.value === 'completed') {
result = result.filter(t => t.completed);
}
// Filter by search
if (searchQuery.value.trim()) {
const query = searchQuery.value.toLowerCase();
result = result.filter(t =>
t.title.toLowerCase().includes(query) ||
t.description?.toLowerCase().includes(query)
);
}
// Sort: open first, then by priority
return [...result].sort((a, b) => {
if (a.completed !== b.completed) return a.completed ? 1 : -1;
const priority = { high: 0, medium: 1, low: 2 };
return priority[a.priority] - priority[b.priority];
});
});
const stats = computed(() => ({
total: tasks.value.length,
completed: tasks.value.filter(t => t.completed).length,
open: tasks.value.filter(t => !t.completed).length
}));
const progress = computed(() => {
if (stats.value.total === 0) return 0;
return Math.round((stats.value.completed / stats.value.total) * 100);
});
const isEmpty = computed(() => tasks.value.length === 0);
const hasOpenTasks = computed(() => stats.value.open > 0);
// ========================================
// ACTIONS
// ========================================
function init() {
loadFromStorage();
// Demo-Daten wenn leer
if (tasks.value.length === 0) {
tasks.value = [
{
id: 1,
title: 'Pinia lernen',
description: 'State Management verstehen',
priority: 'high',
completed: true,
createdAt: new Date().toISOString()
},
{
id: 2,
title: 'Composables erstellen',
description: 'Wiederverwendbare Logik extrahieren',
priority: 'high',
completed: false,
createdAt: new Date().toISOString()
},
{
id: 3,
title: 'Store Actions nutzen',
description: 'CRUD-Operationen im Store',
priority: 'medium',
completed: false,
createdAt: new Date().toISOString()
}
];
}
}
function addTask(taskData) {
const newTask = {
id: Date.now(),
title: taskData.title,
description: taskData.description || '',
priority: taskData.priority || 'medium',
completed: false,
createdAt: new Date().toISOString()
};
tasks.value.unshift(newTask);
return newTask;
}
function updateTask(id, updates) {
const task = tasks.value.find(t => t.id === id);
if (task) {
Object.assign(task, updates);
return true;
}
return false;
}
function deleteTask(id) {
const index = tasks.value.findIndex(t => t.id === id);
if (index !== -1) {
tasks.value.splice(index, 1);
return true;
}
return false;
}
function toggleTask(id) {
const task = tasks.value.find(t => t.id === id);
if (task) {
task.completed = !task.completed;
return task.completed;
}
return null;
}
function setFilter(filter) {
currentFilter.value = filter;
}
function setSearch(query) {
searchQuery.value = query;
}
function clearCompleted() {
tasks.value = tasks.value.filter(t => !t.completed);
}
// ========================================
// RETURN
// ========================================
return {
// State
tasks,
isLoading,
error,
currentFilter,
searchQuery,
// Getters
filteredTasks,
stats,
progress,
isEmpty,
hasOpenTasks,
// Actions
init,
addTask,
updateTask,
deleteTask,
toggleTask,
setFilter,
setSearch,
clearCompleted
};
});
App.vue mit Pinia
<template>
<div class="min-h-screen bg-gradient-to-br from-gray-100 to-gray-200 py-8">
<div class="max-w-2xl mx-auto px-4">
<!-- Header -->
<header class="text-center mb-8">
<h1 class="text-4xl font-bold text-gray-800">📝 Task Manager</h1>
<p class="text-gray-600 mt-2">Vue.js Teil 7: State Management</p>
<p class="text-sm text-gray-500 mt-1">
Powered by 🍍 Pinia
</p>
</header>
<!-- Task Form -->
<TaskForm />
<!-- Stats -->
<TaskStats />
<!-- Search & Filters -->
<div class="mb-4">
<SearchInput
:model-value="taskStore.searchQuery"
@update:model-value="taskStore.setSearch"
placeholder="Tasks durchsuchen..."
/>
</div>
<TaskFilters />
<!-- Task List -->
<TaskList />
<!-- Clear Completed Button -->
<div
v-if="taskStore.stats.completed > 0"
class="text-center mt-6"
>
<button
@click="handleClearCompleted"
class="text-sm text-gray-500 hover:text-red-500 transition-colors"
>
🗑️ {{ taskStore.stats.completed }} erledigte Task(s) löschen
</button>
</div>
<!-- Toast Container -->
<ToastContainer />
<!-- Footer -->
<footer class="text-center mt-12 text-gray-500 text-sm">
<p>Nova & Kat | Java Fleet Systems Consulting</p>
</footer>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
import { useTaskStore } from './stores/taskStore';
import { useToast } from './composables/useToast';
import TaskForm from './components/TaskForm.vue';
import TaskStats from './components/TaskStats.vue';
import TaskFilters from './components/TaskFilters.vue';
import TaskList from './components/TaskList.vue';
import SearchInput from './components/SearchInput.vue';
import ToastContainer from './components/ToastContainer.vue';
const taskStore = useTaskStore();
const toast = useToast();
onMounted(() => {
taskStore.init();
});
function handleClearCompleted() {
const count = taskStore.stats.completed;
taskStore.clearCompleted();
toast.success(`${count} Task(s) gelöscht!`);
}
</script>
🔧 DevTools Integration
Pinia DevTools
Pinia integriert sich automatisch in Vue DevTools!
F12 → Vue Tab → Pinia Tab
├── tasks (Store)
│ ├── State: { tasks: [...], filter: 'all', ... }
│ ├── Getters: { filteredTasks: [...], stats: {...} }
│ └── Actions: addTask, deleteTask, toggleTask, ...
Features:
- 🔍 State live inspizieren
- ⏱️ Time-Travel Debugging
- ✏️ State direkt editieren
- 📊 Action-History
Nova: „Ich kann den State LIVE sehen und ändern?!“
Kat: „Ja! Das ist SO hilfreich beim Debuggen!“
📦 Store-Organisation bei größeren Apps
Mehrere Stores
src/stores/ ├── index.js ← Re-exports ├── taskStore.js ← Tasks ├── userStore.js ← User/Auth ├── settingsStore.js ← App-Settings └── notificationStore.js ← Notifications
// stores/index.js
export { useTaskStore } from './taskStore';
export { useUserStore } from './userStore';
export { useSettingsStore } from './settingsStore';
Store-Kommunikation
// stores/taskStore.js
import { useUserStore } from './userStore';
export const useTaskStore = defineStore('tasks', () => {
const userStore = useUserStore();
function addTask(data) {
// Nutze User-Daten aus anderem Store
const newTask = {
...data,
createdBy: userStore.currentUser?.name || 'Anonymous'
};
tasks.value.push(newTask);
}
return { addTask };
});
❓ FAQ (Häufige Fragen)
Frage 1: Composable vs Store – Was ist der Unterschied?
Antwort:
- Composable: Wiederverwendbare LOGIK (jeder Aufruf = neue Instanz)
- Store: Geteilter STATE (singleton, alle Components sehen dasselbe)
Frage 2: Wann brauche ich Pinia?
Antwort: Wenn mehrere UNABHÄNGIGE Components denselben State brauchen. Beispiele: User-Auth, Shopping Cart, App-Settings. Für 2-3 Components mit Parent-Child-Beziehung reichen Props + Emits!
Frage 3: Kann ich Composables IN Stores nutzen?
Antwort: JA! Das ist sogar Best Practice!
export const useTaskStore = defineStore('tasks', () => {
const { debounce } = useDebounce();
const searchDebounced = debounce(search, 300);
});
Frage 4: Wie teste ich Stores?
Antwort: Mit Vitest + @pinia/testing:
import { setActivePinia, createPinia } from 'pinia';
beforeEach(() => setActivePinia(createPinia()));
Frage 5: Setup Store vs Options Store?
Antwort: Wir nutzen Setup Store (Composition API Style). Es gibt auch Options Stores, aber Setup ist flexibler und TypeScript-freundlicher!
Frage 6: Wie handle ich async Actions?
Antwort: Ganz normal mit async/await! Pinia unterstützt das nativ:
async function fetchTasks() {
isLoading.value = true;
const response = await fetch('/api/tasks');
tasks.value = await response.json();
isLoading.value = false;
}
Frage 7: Wie teilt ihr Verantwortlichkeiten im Team?
Antwort: Gute Frage! State Management ist wie… Team-Kommunikation. Wer ist für was verantwortlich? Wer hat Zugriff auf welche Daten? Manchmal wünschte ich, es gäbe einen teamStore mit klaren actions für Konfliktlösung. 😅 Real talk: Die echten „State-Probleme“ im Team sind nicht technisch… die findet ihr eher in unseren „private logs“. Manchmal ist menschlicher State schwieriger zu managen als App-State.
Frage 8: Brauche ich Pinia für kleine Apps?
Antwort: NEIN! Für kleine Apps reichen Props + Emits + vielleicht ein Composable. Pinia erst wenn die App wächst und Props-Drilling nervt.
Frage 9: Ist Vuex noch relevant?
Antwort: Vuex ist der Vorgänger von Pinia. Für NEUE Projekte: Immer Pinia! Für bestehende Vuex-Apps: Migration ist möglich, aber nicht dringend.
Frage 10: Wie strukturiere ich große Stores?
Antwort: Aufteilen! Ein Store pro „Domain“: userStore, productStore, cartStore. Nicht alles in einen Mega-Store!
📖 Vue.js für Anfänger – Alle Teile im Überblick
✅ Bereits veröffentlicht:
- Teil 1 (14.10.2025): Setup & Erste API-Anbindung
- Teil 2 (21.10.2025): Styling & Responsive Design
- Teil 3 (28.10.2025): Formulare & User Input
- Teil 4 (04.11.2025): Listen-Manipulation & Reaktivität
- Teil 5 (11.11.2025): CRUD Operations Complete
- Teil 6 (18.11.2025): Component Communication
- Teil 7 (25.11.2025): State Management – Du bist hier! ✨
🔜 Kommende Teile:
- Teil 8 (02.12.2025): Routing & Multi-Page Apps
- Teil 9 (09.12.2025): API Integration & Error Handling
- Teil 10 (16.12.2025): Testing mit Vitest
- Teil 11 (23.12.2025): Performance & Production-Ready
- Teil 12 (30.12.2025): Nova’s eigenes Projekt „Book Buddy“ 🎓
🔧 Tools & Ressourcen
Offizielle Docs:
| Ressource | Link |
|---|---|
| Pinia Docs | https://pinia.vuejs.org/ |
| Pinia Getting Started | https://pinia.vuejs.org/getting-started.html |
| Vue Composition API | https://vuejs.org/guide/reusability/composables.html |
| VueUse | https://vueuse.org/ (Composables-Sammlung!) |
DevTools:
| Tool | Link |
|---|---|
| Vue DevTools (Chrome) | https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd |
| Vue DevTools (Firefox) | https://addons.mozilla.org/de/firefox/addon/vue-js-devtools/ |
💬 Real Talk: State-Chaos
Java Fleet Küche, 13:00 Uhr. Nova starrt auf ihren Bildschirm, Kat kommt mit Kaffee vorbei. Cassian sitzt am Nebentisch mit seinem Laptop.
Nova: „Kat, ich hab’s kapiert! Pinia ist wie… ein zentrales Gehirn für die App!“
Kat: [setzt sich] „Gute Analogie. Ein Store pro Themenbereich.“
Cassian: [schaut auf] „Ihr redet über State Management?“
Nova: „Ja! Ich hatte überall duplizierten Code. Jetzt ist alles im Store!“
Cassian: „Das kenne ich. Bei ML-Projekten ist State noch komplexer. Model-Weights, Training-Progress, Hyperparameters…“
Kat: „Nutzt ihr auch Pinia?“
Cassian: „Manchmal. Aber oft Composables mit Singleton-Pattern. Kommt auf den Use Case an.“
Nova: „Was ist besser?“
Cassian: „Falsche Frage. Es kommt darauf an, was du brauchst. Composables für Logik, Stores für geteilten State.“
Kat: „Das hab ich auch gesagt!“
Nova: „Okay, aber… wann weiß ich, dass ich einen Store BRAUCHE?“
Cassian: „Wenn du anfängst, Props durch 4+ Ebenen zu reichen. Oder wenn du merkst, dass mehrere unabhängige Components denselben State brauchen.“
Kat: „Props-Drilling ist das Warnsignal.“
Nova: [nickt] „Verstanden. Aber jetzt hab ich ein anderes Problem: Mein Store wird RIESIG!“
Cassian: „Aufteilen. Ein Store pro Domain. taskStore, userStore, settingsStore.“
Kat: „Genau. Wie Microservices, nur für State.“
[Kurze Pause. Cassian schaut nachdenklich.]
Cassian: „Weißt du, Nova… State Management im Code ist eigentlich simpel. Du hast klare Regeln, Actions, Getters. Aber State im echten Leben?“
Nova: „Was meinst du?“
Cassian: „Naja… Beziehungen, Teamdynamik, persönliche Ziele. Da gibt’s keine actions die du einfach aufrufen kannst.“
Kat: [schmunzelt] „Du bist heute wieder philosophisch.“
Cassian: „ML-Modelle machen das mit mir. Sie optimieren Zustände. Ich frag mich manchmal, ob Menschen das auch können.“
Nova: „Deep. Aber ich bleib erstmal bei Pinia!“
Kat: [lacht] „Smart. Und nächste Woche: Routing! Mehrere Seiten in einer App!“
Nova: „Endlich! Ich wollte schon ewig eine zweite Seite haben!“
🎁 Bonus: State Management Cheat Sheet
// ===== COMPOSABLE (Wiederverwendbare Logik) =====
// composables/useLocalStorage.js
import { ref, watch } from 'vue';
export function useLocalStorage(key, defaultValue) {
const data = ref(JSON.parse(localStorage.getItem(key)) ?? defaultValue);
watch(data, (val) => localStorage.setItem(key, JSON.stringify(val)), { deep: true });
return data;
}
// Verwendung:
const tasks = useLocalStorage('tasks', []);
// ===== PINIA STORE (Geteilter State) =====
// stores/taskStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useTaskStore = defineStore('tasks', () => {
// State
const tasks = ref([]);
// Getters
const completedTasks = computed(() =>
tasks.value.filter(t => t.completed)
);
// Actions
function addTask(task) {
tasks.value.push(task);
}
function deleteTask(id) {
tasks.value = tasks.value.filter(t => t.id !== id);
}
return { tasks, completedTasks, addTask, deleteTask };
});
// Verwendung in Component:
import { useTaskStore } from '@/stores/taskStore';
const taskStore = useTaskStore();
taskStore.addTask({ id: 1, title: 'Neu!' });
// ===== WANN WAS? =====
// Composable: Wiederverwendbare LOGIK
// → useDebounce, useLocalStorage, useFetch
// Pinia: Geteilter STATE
// → taskStore, userStore, cartStore
// Props/Emits: Lokale Component-Kommunikation
// → Parent-Child, 1-3 Ebenen tief
🎨 Challenge für die Community!
Kat’s Herausforderung:
„Du verstehst jetzt State Management! Zeit für eine Challenge:
Level 1 – Composables:
- 📦 Erstelle
useLocalStoragefür Persistence - 🔔 Erstelle
useToastfür Notifications - ⏱️ Erstelle
useDebouncefür Search
Level 2 – Pinia Basics:
- 🍍 Erstelle einen
taskStore - 📊 Implementiere Getters für Stats
- ✏️ Implementiere alle CRUD-Actions
Level 3 – Advanced:
- 🔄 Store-Kommunikation (z.B. taskStore + userStore)
- 💾 Pinia Persistence Plugin
- 🧪 Store-Tests mit Vitest
Teile dein Ergebnis:
- Screenshot der DevTools mit deinem Store
- GitHub-Repo verlinken
- Erkläre deine Store-Struktur!
Die saubersten Architekturen featuren wir! 🎉“
💬 Das war Teil 7 der Vue.js für Anfänger Serie!
Nova & Kat: „Von Copy-Paste zu Clean Architecture! 📋➡️🏗️“
Du hast heute etwas FUNDAMENTALES gelernt! State Management ist das Rückgrat jeder größeren App!
Was du geschafft hast:
✅ Composables verstanden & erstellt ✅ Logik aus Components extrahiert ✅ Pinia installiert & konfiguriert ✅ Stores mit State, Getters, Actions ✅ DevTools Integration genutzt ✅ Wann Composable vs Pinia entschieden
Real talk: Das war ein Level-Up! Composables und Pinia sind das, was deine Apps von „funktioniert“ zu „professionell“ macht. Keine duplizierten Daten mehr, keine Props durch 10 Ebenen, saubere Architektur! 💪
Hast du es nachgebaut? Teile deinen Store!
Fragen? Schreib uns:
- Nova: nova.trent@java-developer.online
- Kat: katharina.schmidt@java-developer.online
Nächster Teil: In 1 Woche! Routing – Mehrere Seiten in einer App! 🚀
Keep coding, keep managing state! 🍍
Nova & Kat – Von Chaos zu Ordnung, gemeinsam! 💚
Tags: #VueJS #Pinia #StateManagement #Composables #Frontend #WebDevelopment #Store #JavaScript
© 2025 Java Fleet Systems Consulting | java-developer.online

