Composition API Fundamentals

Module 14: JavaScript Frontend Frameworks - Vue & Angular

Introduction to the Composition API

The Composition API is a set of APIs introduced in Vue 3 that provides a more flexible way to organize and reuse code in Vue components. It addresses limitations of the Options API, particularly for complex components where related code ends up split across different options.

Real-world analogy: If the Options API is like sorting items into pre-defined containers (data, methods, computed, etc.), the Composition API is like organizing items by their relationships. It's similar to the difference between sorting books by size/color versus grouping them by topic or author.

flowchart LR A["Options API (Organized by Option Type)"] --> B[data] A --> C[methods] A --> D[computed] A --> E[watch] F["Composition API (Organized by Feature)"] --> G["User Feature (state, actions, etc.)"] F --> H["Products Feature (state, actions, etc.)"] F --> I["Cart Feature (state, actions, etc.)"] style A fill:#42b883,color:white style F fill:#35495e,color:white

Options API vs Composition API

Let's compare a simple component implemented with both APIs to understand the differences:

Options API

<script>
export default {
  // State is grouped in data
  data() {
    return {
      count: 0,
      name: 'John',
      isActive: true
    }
  },
  
  // Methods grouped together
  methods: {
    increment() {
      this.count++
    },
    updateName(newName) {
      this.name = newName
    },
    toggleActive() {
      this.isActive = !this.isActive
    }
  },
  
  // Computed properties grouped together
  computed: {
    doubleCount() {
      return this.count * 2
    },
    displayName() {
      return `User: ${this.name}`
    }
  },
  
  // Lifecycle hooks scattered
  mounted() {
    console.log('Component mounted')
  }
}
</script>

Composition API

<script setup>
import { ref, computed, onMounted } from 'vue'

// Counter feature
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
  count.value++
}

// User feature
const name = ref('John')
const displayName = computed(() => `User: ${name.value}`)
function updateName(newName) {
  name.value = newName
}

// Activity feature
const isActive = ref(true)
function toggleActive() {
  isActive.value = !isActive.value
}

// Lifecycle hook
onMounted(() => {
  console.log('Component mounted')
})
</script>

Key Differences and Advantages

Important: The Composition API doesn't replace the Options API – both are fully supported in Vue 3. You can use either one or even mix them in the same project based on your preference and needs.

setup Function and script setup Syntax

There are two main ways to use the Composition API in Vue 3:

1. The setup() Function

The setup() function runs before the component instance is created, replacing the role of data, computed, methods, watch, and lifecycle hooks from the Options API:

<script>
import { ref, computed, onMounted } from 'vue'

export default {
  props: {
    initialCount: Number
  },
  setup(props, context) {
    // State
    const count = ref(props.initialCount || 0)
    
    // Computed property
    const doubleCount = computed(() => count.value * 2)
    
    // Method
    function increment() {
      count.value++
    }
    
    // Lifecycle hook
    onMounted(() => {
      console.log('Component mounted')
    })
    
    // Expose to the template (everything returned is available)
    return {
      count,
      doubleCount,
      increment
    }
  }
}
</script>

2. The <script setup> Syntax (Recommended)

Vue 3.2 introduced a simplified syntax using <script setup>, which is now the recommended approach:

<script setup>
import { ref, computed, onMounted } from 'vue'

// Props are declared with defineProps macro
const props = defineProps({
  initialCount: Number
})

// State
const count = ref(props.initialCount || 0)

// Computed property
const doubleCount = computed(() => count.value * 2)

// Method
function increment() {
  count.value++
}

// Lifecycle hook
onMounted(() => {
  console.log('Component mounted')
})

// No need to return anything - all top-level bindings
// are automatically exposed to the template
</script>

Advantages of <script setup>:

  • More concise code with less boilerplate
  • Ability to declare props and emits with compile-time macros
  • Better runtime performance (no need for a separate function called for each component instance)
  • Better IDE type-inference and completion
  • All variables/functions are automatically available in template

Reactivity Fundamentals

At the core of the Composition API is Vue's reactivity system, which allows the UI to automatically update when the underlying data changes.

Reactive References with ref()

ref() creates a reactive reference for a value of any type. The returned object has a single property .value that holds the inner value.

import { ref } from 'vue'

// Primitive values
const count = ref(0)
const name = ref('John')
const isActive = ref(true)

// Accessing and modifying
console.log(count.value) // 0
count.value++
console.log(count.value) // 1

// Complex values
const user = ref({
  name: 'John',
  age: 30
})

// Accessing and modifying
console.log(user.value.name) // 'John'
user.value.age = 31

Important: Inside templates, the .value property is automatically unwrapped for convenience:

<template>
  <div>
    <p>Count: {{ count }}</p>      <!-- No .value needed in templates -->
    <p>User: {{ user.name }}</p>   <!-- Objects are also unwrapped automatically -->
    <button @click="increment">Increment</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)
const user = ref({ name: 'John', age: 30 })

function increment() {
  count.value++ // .value is required in JavaScript
}
</script>

Real-world analogy: A ref is like a special container that notifies Vue when its contents change. Think of it as a display case in a museum – the case itself stays the same, but when you swap the item inside, sensors detect the change and update related information displays.

Reactive Objects with reactive()

reactive() makes an entire object reactive. Unlike ref, you don't need to use .value to access or modify properties:

import { reactive } from 'vue'

const user = reactive({
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    zip: '10001'
  }
})

// Direct access (no .value needed)
console.log(user.name) // 'John'
user.age = 31
user.address.city = 'Boston'

ref vs. reactive: When to Use Each

Use ref when

  • Working with primitive values (strings, numbers, booleans)
  • You need to pass reactive values to functions
  • You want the flexibility to reassign the entire value
  • You need to create reactive references to props

Use reactive when

  • Working with objects or arrays exclusively
  • You don't need to replace the entire object, just modify its properties
  • You want a more natural syntax for object manipulation
  • You're working with deeply nested objects

Common practice: Many Vue developers use ref for nearly everything to maintain consistency and avoid the limitations of reactive (like losing reactivity when objects are destructured).

Reactivity Caveats

There are some important edge cases to be aware of:

1. Destructuring Reactive Objects: When you destructure a reactive object, the connection to the original reactive object is lost:

const user = reactive({ name: 'John', age: 30 })

// This breaks reactivity!
const { name, age } = user

// These won't update the reactive user object
name = 'Jane'
age = 31

Solution: Use toRefs or toRef to maintain reactivity:

import { reactive, toRefs, toRef } from 'vue'

const user = reactive({ name: 'John', age: 30 })

// Keep reactivity with toRefs (for multiple properties)
const { name, age } = toRefs(user)
name.value = 'Jane' // This updates user.name!

// Or use toRef for a single property
const name = toRef(user, 'name')
name.value = 'Jane' // This updates user.name!

2. Collection Types: Vue cannot detect property addition or deletion for objects:

const user = reactive({ name: 'John' })

// This won't trigger updates in templates!
user.age = 30

Solution: Use the spread operator to create a new object or use Vue's utilities:

import { reactive } from 'vue'

let user = reactive({ name: 'John' })

// Method 1: Create a new reactive object
user = reactive({ ...user, age: 30 })

// Method 2: Use Object.assign with reactive objects
Object.assign(user, { age: 30 })

Computed Properties

Computed properties allow you to create derived state that updates automatically when its dependencies change.

import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// Simple computed property
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})

console.log(fullName.value) // 'John Doe'

// Changing dependency automatically updates computed value
firstName.value = 'Jane'
console.log(fullName.value) // 'Jane Doe'

Writable Computed Properties

Computed properties can also have getters and setters:

import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  // getter
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  // setter
  set(newValue) {
    const names = newValue.split(' ')
    firstName.value = names[0]
    lastName.value = names[names.length - 1]
  }
})

console.log(fullName.value) // 'John Doe'

// Setting the computed property
fullName.value = 'Jane Smith'

console.log(firstName.value) // 'Jane'
console.log(lastName.value) // 'Smith'

Real-world use case: Writable computed properties are useful for creating derived values that can be manipulated, like formatted inputs (e.g., phone numbers, currencies) or complex state that affects multiple underlying values.

Watchers

Watchers allow you to perform side effects in response to changes in reactive state.

Basic watch

import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('')

// Watch for changes to question
watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.trim() !== '') {
    answer.value = 'Thinking...'
    try {
      const response = await fetch(`/api/answer?q=${encodeURIComponent(newQuestion)}`)
      const data = await response.json()
      answer.value = data.answer
    } catch (error) {
      answer.value = 'Error! Could not reach the API.'
    }
  } else {
    answer.value = ''
  }
})

Watching Multiple Sources

import { ref, watch } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// Watch multiple sources
watch([firstName, lastName], ([newFirstName, newLastName], [oldFirstName, oldLastName]) => {
  console.log(`Name changed from ${oldFirstName} ${oldLastName} to ${newFirstName} ${newLastName}`)
})

Deep Watchers

import { reactive, watch } from 'vue'

const user = reactive({
  name: 'John',
  profile: {
    age: 30,
    preferences: {
      newsletter: true,
      theme: 'dark'
    }
  }
})

// Deep watch (detects nested property changes)
watch(user, (newValue, oldValue) => {
  console.log('User object changed:', newValue)
}, { deep: true })

// This will trigger the watcher
user.profile.preferences.theme = 'light'

Immediate Watchers

import { ref, watch } from 'vue'

const searchQuery = ref('vue')
const results = ref([])

// Run immediately and on every change
watch(searchQuery, async (newQuery) => {
  if (newQuery.trim()) {
    const response = await fetch(`/api/search?q=${encodeURIComponent(newQuery)}`)
    results.value = await response.json()
  } else {
    results.value = []
  }
}, { immediate: true })

watchEffect for Automatic Dependency Tracking

watchEffect runs immediately and automatically tracks all reactive dependencies used within it, re-running whenever any of them change:

import { ref, watchEffect } from 'vue'

const userId = ref(1)
const userData = ref(null)

// Automatically tracks dependencies (userId in this case)
watchEffect(async () => {
  if (userId.value) {
    const response = await fetch(`/api/users/${userId.value}`)
    userData.value = await response.json()
  } else {
    userData.value = null
  }
})

// Later, changing userId will trigger the effect again
userId.value = 2

Real-world use case: watchEffect is particularly useful for side effects that depend on multiple reactive values, like data fetching, caching, or syncing to local storage.

Stopping Watchers

Both watch and watchEffect return a function that stops the watcher when called:

const stopWatch = watch(source, callback)

// Later, when no longer needed
stopWatch()

const stopEffect = watchEffect(() => {
  // ...
})

// Later, when no longer needed
stopEffect()

Lifecycle Hooks

The Composition API provides equivalents for all lifecycle hooks from the Options API, prefixed with "on":

flowchart LR A[beforeCreate/created] --> B["setup()"] C[beforeMount] --> D["onBeforeMount()"] E[mounted] --> F["onMounted()"] G[beforeUpdate] --> H["onBeforeUpdate()"] I[updated] --> J["onUpdated()"] K[beforeUnmount] --> L["onBeforeUnmount()"] M[unmounted] --> N["onUnmounted()"] O[errorCaptured] --> P["onErrorCaptured()"] style B fill:#42b883,color:white style D fill:#42b883,color:white style F fill:#42b883,color:white style H fill:#42b883,color:white style J fill:#42b883,color:white style L fill:#42b883,color:white style N fill:#42b883,color:white style P fill:#42b883,color:white
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured
} from 'vue'

export default {
  setup() {
    // created is replaced by the setup function itself
    console.log('Component instance created')
    
    onBeforeMount(() => {
      console.log('Before mounting DOM')
    })
    
    onMounted(() => {
      console.log('DOM mounted')
      // Access DOM elements, initialize libraries, etc.
    })
    
    onBeforeUpdate(() => {
      console.log('Before re-rendering due to data change')
    })
    
    onUpdated(() => {
      console.log('Re-rendered after data change')
    })
    
    onBeforeUnmount(() => {
      console.log('Before component unmount')
      // Clean up resources
    })
    
    onUnmounted(() => {
      console.log('Component unmounted')
    })
    
    onErrorCaptured((err, instance, info) => {
      console.error('Error captured:', err)
      // Return false to prevent error propagation
      return false
    })
    
    // Additional Vue 3 hooks
    
    // Triggered when a component inside  is toggled
    onActivated(() => {
      console.log('Component activated from cache')
    })
    
    onDeactivated(() => {
      console.log('Component deactivated to cache')
    })
    
    // For debugging and performance tracking
    onRenderTracked((event) => {
      console.log('Dependency tracked:', event)
    })
    
    onRenderTriggered((event) => {
      console.log('Re-render triggered by:', event)
    })
    
    // Rest of setup...
  }
}

Note: There are no Composition API equivalents for beforeCreate and created because the setup() function itself serves this purpose. Any code at the top level of setup() or <script setup> is executed during the component's creation phase.

Template Refs

Template refs provide a way to access underlying DOM elements or child component instances directly.

<template>
  <div>
    <input ref="inputRef">
    <button @click="focusInput">Focus Input</button>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

// Create a ref with the same name as the template ref
const inputRef = ref(null)

// Access the element in a lifecycle hook
onMounted(() => {
  // inputRef.value now points to the actual DOM element
  inputRef.value.focus()
})

function focusInput() {
  inputRef.value.focus()
}
</script>

Refs on Components

When used on a component, the ref will point to the component instance:

<template>
  <ChildComponent ref="childRef" />
  <button @click="callChildMethod">Call Child Method</button>
</template>

<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const childRef = ref(null)

function callChildMethod() {
  // Access public methods on the child component
  childRef.value.somePublicMethod()
}
</script>

v-for with Refs

When using ref with v-for, you'll need an array to store the elements:

<template>
  <ul>
    <li v-for="(item, index) in items" :key="item.id" :ref="el => itemRefs[index] = el">
      {{ item.text }}
    </li>
  </ul>
</template>

<script setup>
import { ref, onMounted, reactive } from 'vue'

const items = ref([
  { id: 1, text: 'Item 1' },
  { id: 2, text: 'Item 2' },
  { id: 3, text: 'Item 3' }
])

// Array to store element references
const itemRefs = reactive([])

onMounted(() => {
  console.log(itemRefs) // Array of li elements
})
</script>

Composables: Reusing Logic

One of the main benefits of the Composition API is the ability to extract and reuse logic across components through "composable" functions.

Real-world analogy: Think of composables like kitchen appliances. Each appliance (blender, mixer, etc.) handles a specific task and can be used in many different recipes. Similarly, composables are reusable pieces of logic that can be combined to build complex functionality.

Creating a Composable Function

A composable is a function that leverages Vue's Composition API to encapsulate and reuse stateful logic:

// useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0, step = 1) {
  // State
  const count = ref(initialValue)
  
  // Computed
  const doubleCount = computed(() => count.value * 2)
  
  // Methods
  function increment() {
    count.value += step
  }
  
  function decrement() {
    count.value -= step
  }
  
  function reset() {
    count.value = initialValue
  }
  
  // Return everything needed by components
  return {
    count,
    doubleCount,
    increment,
    decrement,
    reset
  }
}

Using a Composable in a Component

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
    <button @click="reset">Reset</button>
  </div>
</template>

<script setup>
import { useCounter } from './composables/useCounter'

// Use the composable
const { count, doubleCount, increment, decrement, reset } = useCounter(10, 2)
</script>

Another Example: Fetch Data Composable

// useFetch.js
import { ref, watchEffect, toValue, isRef } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)
  
  async function fetchData() {
    loading.value = true
    error.value = null
    
    try {
      // Handle both ref and direct string
      const response = await fetch(toValue(url))
      if (!response.ok) {
        throw new Error(`Network error: ${response.status}`)
      }
      data.value = await response.json()
    } catch (err) {
      error.value = err.message || 'Unknown error'
    } finally {
      loading.value = false
    }
  }
  
  // If url is a ref, react to changes, otherwise just fetch once
  if (isRef(url)) {
    watchEffect(fetchData)
  } else {
    fetchData()
  }
  
  // Allow manual refresh
  function refresh() {
    return fetchData()
  }
  
  return { data, error, loading, refresh }
}

Using the Fetch Composable

<template>
  <div>
    <select v-model="selectedId">
      <option value="1">User 1</option>
      <option value="2">User 2</option>
      <option value="3">User 3</option>
    </select>
    
    <div v-if="loading">Loading...</div>
    <div v-else-if="error">Error: {{ error }}</div>
    <div v-else-if="data">
      <h2>{{ data.name }}</h2>
      <p>Email: {{ data.email }}</p>
    </div>
    
    <button @click="refresh">Refresh</button>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useFetch } from './composables/useFetch'

const selectedId = ref('1')

// Create a computed URL based on the selected ID
const userUrl = computed(() => `https://jsonplaceholder.typicode.com/users/${selectedId.value}`)

// Use the fetch composable with a reactive URL
const { data, error, loading, refresh } = useFetch(userUrl)
</script>

Composables vs. Mixins

Vue 2 used mixins for code reuse, but they had several drawbacks that composables address:

Mixins (Vue 2)

  • Unclear source of properties (which mixin provided what)
  • Naming conflicts between mixins
  • Implicit dependency relationships
  • Limited customization

Composables (Vue 3)

  • Explicit imports make the source clear
  • Variable renaming solves naming conflicts
  • Clear dependency chain
  • More customizable through function parameters

Complete Example: Task Manager with Composition API

Let's bring all these concepts together with a complete example of a task management application:

1. Task Management Composable

// composables/useTasks.js
import { ref, computed } from 'vue'

export function useTasks() {
  // State
  const tasks = ref([])
  const filter = ref('all') // all, active, completed
  
  // Computed
  const filteredTasks = computed(() => {
    switch (filter.value) {
      case 'active':
        return tasks.value.filter(task => !task.completed)
      case 'completed':
        return tasks.value.filter(task => task.completed)
      default:
        return tasks.value
    }
  })
  
  const remainingCount = computed(() => 
    tasks.value.filter(task => !task.completed).length
  )
  
  // Methods
  function addTask(text) {
    if (text.trim()) {
      tasks.value.push({
        id: Date.now(),
        text,
        completed: false
      })
    }
  }
  
  function removeTask(id) {
    tasks.value = tasks.value.filter(task => task.id !== id)
  }
  
  function toggleTask(id) {
    const task = tasks.value.find(task => task.id === id)
    if (task) {
      task.completed = !task.completed
    }
  }
  
  function updateTaskText(id, text) {
    const task = tasks.value.find(task => task.id === id)
    if (task && text.trim()) {
      task.text = text
    }
  }
  
  function setFilter(newFilter) {
    filter.value = newFilter
  }
  
  function clearCompleted() {
    tasks.value = tasks.value.filter(task => !task.completed)
  }
  
  return {
    tasks,
    filter,
    filteredTasks,
    remainingCount,
    addTask,
    removeTask,
    toggleTask,
    updateTaskText,
    setFilter,
    clearCompleted
  }
}

2. Local Storage Persistence Composable

// composables/useLocalStorage.js
import { watch } from 'vue'

export function useLocalStorage(key, state) {
  // Load initial state from localStorage if it exists
  try {
    const storedValue = localStorage.getItem(key)
    if (storedValue) {
      state.value = JSON.parse(storedValue)
    }
  } catch (err) {
    console.error(`Error reading ${key} from localStorage:`, err)
  }
  
  // Watch for changes and update localStorage
  watch(
    state,
    (newValue) => {
      try {
        localStorage.setItem(key, JSON.stringify(newValue))
      } catch (err) {
        console.error(`Error saving ${key} to localStorage:`, err)
      }
    },
    { deep: true }
  )
  
  return state
}

3. Main App Component

<template>
  <div class="todo-app">
    <h1>Task Manager</h1>
    
    <!-- Task Input -->
    <div class="task-input">
      <input 
        ref="inputRef"
        v-model="newTask" 
        @keyup.enter="handleAddTask"
        placeholder="What needs to be done?"
      >
      <button @click="handleAddTask">Add</button>
    </div>
    
    <!-- Filter Controls -->
    <div class="task-filters">
      <button 
        :class="{ active: filter === 'all' }"
        @click="setFilter('all')"
      >All</button>
      
      <button 
        :class="{ active: filter === 'active' }"
        @click="setFilter('active')"
      >Active</button>
      
      <button 
        :class="{ active: filter === 'completed' }"
        @click="setFilter('completed')"
      >Completed</button>
    </div>
    
    <!-- Task List -->
    <ul class="task-list">
      <li v-for="task in filteredTasks" :key="task.id" class="task-item">
        <div v-if="editingId === task.id" class="task-edit">
          <input 
            v-model="editText" 
            @blur="finishEditing" 
            @keyup.enter="finishEditing"
            @keyup.escape="cancelEditing"
            ref="editInputRef"
          >
        </div>
        
        <div v-else class="task-view">
          <input 
            type="checkbox" 
            :checked="task.completed"
            @change="toggleTask(task.id)"
          >
          
          <span 
            :class="{ completed: task.completed }"
            @dblclick="startEditing(task)"
          >
            {{ task.text }}
          </span>
          
          <button @click="removeTask(task.id)" class="delete-btn">×</button>
        </div>
      </li>
    </ul>
    
    <!-- Task Summary -->
    <div class="task-summary" v-if="tasks.length > 0">
      <span>{{ remainingCount }} items left</span>
      <button @click="clearCompleted" v-if="tasks.length > remainingCount">
        Clear completed
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, nextTick } from 'vue'
import { useTasks } from './composables/useTasks'
import { useLocalStorage } from './composables/useLocalStorage'

// Task management logic
const { 
  tasks, 
  filter, 
  filteredTasks, 
  remainingCount,
  addTask,
  removeTask,
  toggleTask,
  updateTaskText,
  setFilter,
  clearCompleted
} = useTasks()

// Persist tasks to localStorage
useLocalStorage('tasks', tasks)

// Input refs
const inputRef = ref(null)
const editInputRef = ref(null)

// Task input state
const newTask = ref('')

// Editing state
const editingId = ref(null)
const editText = ref('')

// Focus the main input on mount
onMounted(() => {
  inputRef.value.focus()
})

// Add task and clear input
function handleAddTask() {
  if (newTask.value.trim()) {
    addTask(newTask.value)
    newTask.value = ''
    inputRef.value.focus()
  }
}

// Start editing a task
function startEditing(task) {
  editingId.value = task.id
  editText.value = task.text
  
  // Focus the edit input after DOM update
  nextTick(() => {
    if (editInputRef.value) {
      editInputRef.value.focus()
    }
  })
}

// Finish editing and save changes
function finishEditing() {
  if (editingId.value !== null) {
    updateTaskText(editingId.value, editText.value)
    editingId.value = null
  }
}

// Cancel editing without saving
function cancelEditing() {
  editingId.value = null
}
</script>

<style scoped>
.todo-app {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
}

/* Other styles... */
</style>

This example demonstrates:

Activities for Practice

Exercise 1: Convert Options API to Composition API

Take a component written with the Options API and convert it to use the Composition API with <script setup>.

Start with this component:

<template>
  <div>
    <h2>{{ title }}</h2>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
    <button @click="reset">Reset</button>
    <input v-model="title">
  </div>
</template>

<script>
export default {
  props: {
    initialCount: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      count: this.initialCount,
      title: 'Counter Component'
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    },
    reset() {
      this.count = this.initialCount
    }
  },
  mounted() {
    console.log('Component mounted')
  },
  watch: {
    count(newValue, oldValue) {
      console.log(`Count changed from ${oldValue} to ${newValue}`)
    }
  }
}
</script>

Exercise 2: Create a Composition API Form Validation

Create a form validation system using the Composition API:

  1. Create a useFormValidation composable that:
    • Accepts an object with form fields and validation rules
    • Returns validation state, errors, and a validate function
    • Supports rules like 'required', 'email', 'minLength', etc.
  2. Build a registration form component that uses this composable with:
    • Name (required)
    • Email (required, email format)
    • Password (required, min 8 characters)
    • PasswordConfirm (must match password)
  3. Display validation errors in the UI
  4. Only enable the submit button when all validations pass

Exercise 3: Build a Composable for API Requests

Extend the useFetch composable to create a more comprehensive API request composable:

  1. Create a useApi composable that supports:
    • Multiple HTTP methods (GET, POST, PUT, DELETE)
    • Authentication headers
    • Request/response interceptors
    • Automatic error handling
    • Request cancellation
  2. Build a simple UI that demonstrates:
    • Loading states during requests
    • Displaying data when successful
    • Showing error messages on failure
    • Cancelling a request in progress

You can use free APIs like JSONPlaceholder or a mock API for testing.

Additional Resources