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

SituationLösungWarum?
Wiederverwendbare Logik (nicht State)Composablez.B. useDebounce, useEventListener
Lokaler Component-Stateref()Nur eine Component braucht es
Geteilter State zwischen WENIGEN ComponentsComposable mit Singletonz.B. useToast (global)
Globaler App-StatePinia StoreTasks, User, Cart, etc.
Server-State (API-Daten)Pinia + ComposableStore 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:

RessourceLink
Pinia Docshttps://pinia.vuejs.org/
Pinia Getting Startedhttps://pinia.vuejs.org/getting-started.html
Vue Composition APIhttps://vuejs.org/guide/reusability/composables.html
VueUsehttps://vueuse.org/ (Composables-Sammlung!)

DevTools:

ToolLink
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 useLocalStorage für Persistence
  • 🔔 Erstelle useToast für Notifications
  • ⏱️ Erstelle useDebounce fü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

Autoren

  • Ensign Nova Trent

    24 Jahre alt, frisch von der Universität als Junior Entwicklerin bei Java Fleet Systems Consulting. Nova ist brilliant in Algorithmen und Datenstrukturen, aber neu in der praktischen Java-Enterprise-Entwicklung. Sie brennt darauf, ihre ersten echten Projekte zu bauen und entdeckt dabei die Lücke zwischen Uni-Theorie und Entwickler-Realität. Sie liebt Star Treck das ist der Grund warum alle Sie Ensign Nova nennen und arbeitet daraufhin das sie Ihren ersten Knopf am Kragen bekommt.

  • Katharina Schmidt

    Katharina „Kat“ Schmidt – Die Design-Denkerin

    Senior Frontend- & UI/UX-Developerin | 29 Jahre | „Design ist Kommunikation – nicht Dekoration.“

    Kat ist das Gesicht hinter dem, was Nutzer sehen, fühlen und anklicken.
    In einem Team, das tief in Backend-Architekturen, Security und Data Flows denkt, sorgt sie dafür, dass der Mensch nicht vergessen wird.
    Ihre Arbeitsweise ist wie ihr Stil: präzise, ruhig, ästhetisch – und immer auf das Wesentliche reduziert.

    Sie kam 2022 zu Java Fleet, mit einem Hintergrund in Medieninformatik und einer klaren Mission: gute Technik sichtbar machen.
    Sie übersetzt komplexe Backend-Prozesse in klare Interfaces, macht aus endlosen Formularen strukturierte Nutzerreisen – und schafft es, dass ein Button „richtig“ wirkt, ohne dass jemand erklären kann, warum.

    💻 Die Tech-Seite

    Kat beherrscht den kompletten modernen Frontend-Stack:
    React, Vue.js, Tailwind CSS, SASS, Figma, Storybook – sie baut Systeme, keine Einzelkomponenten.
    Für sie ist Design kein Add-on, sondern ein integraler Teil der Architektur.
    Wenn sie an einer Oberfläche arbeitet, denkt sie wie ein Entwickler; wenn sie mit Entwicklern spricht, denkt sie wie eine Nutzerin.

    „Design ohne Verständnis für Code ist Deko. Code ohne Verständnis für Menschen ist Selbstgespräch.“

    Kat liebt es, Barrieren zu entfernen – technische, visuelle und emotionale.
    Accessibility (WCAG 2.1), Responsiveness, Semantik – das sind für sie keine Checklisten, sondern Selbstverständlichkeiten.

    🌿 Die menschliche Seite

    Kat ist der kreative Ruhepol im Team.
    Sie arbeitet konzentriert, lacht leise, und wenn sie eine Idee erklärt, spürt man sofort: Sie hat sie nicht nur verstanden – sie hat sie durchdacht.
    In ihrem Büro stehen eine Zimmerpflanze, ein Notizblock mit Skizzen, ein Grafiktablett und eine große Tasse Tee.
    Wenn sie nachdenkt, zeichnet sie. Wenn sie zuhört, beobachtet sie.

    Sie ist die empathischste Entwicklerin im Raum – und genau das macht sie so wichtig.
    Nova nennt sie „meine UI-Großschwester“, Cassian bewundert ihre gedankliche Klarheit, und Franz-Martin sagt:

    „Kat sieht das, was wir übersehen – und sie sagt’s, ohne laut zu werden.“

    🧠 Ihre Rolle im Team

    Kat ist die Schnittstelle zwischen Technik und Mensch – zwischen Backend und Benutzer, Code und Kommunikation.
    Sie organisiert Design-System-Workshops, dokumentiert Prinzipien, und sorgt dafür, dass jedes Projekt der Java Fleet ein Gesicht bekommt.
    Dabei denkt sie nicht in Farben oder Fonts, sondern in Abläufen und Emotionen: Wie fühlt sich ein Login an? Wann entsteht Vertrauen?
    Ihr Ziel: Interfaces, die sich richtig anfühlen – weil sie logisch sind.

    ⚡ Superkraft

    Empathie in Codeform.
    Kat übersetzt komplexe Technik in nutzerfreundliche Realität – elegant, barrierefrei und ehrlich.

    💬 Motto

    „Design ist, wenn du nicht erklären musst, wie es funktioniert.“