#vue #javascript #frontend

Day 9: Reusability with Composables

Welcome to Day 9! If you were using Vue 2, you might remember “Mixins”. Composables essentially replace Mixins, but better (no name collisions, explicit dependencies).

What is a Composable?

It handles stateful logic. It’s just a function that uses ref, computed, or watch and returns them.

By convention, use the prefix use... (e.g., useMouse, useFetch).

Example: useMouse

composables/useMouse.ts

import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event: MouseEvent) {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // Return the refs so components can use them
  return { x, y }
}

Using it in a Component

<script setup lang="ts">
import { useMouse } from './composables/useMouse'

// Hooks into the component's lifecycle automatically!
const { x, y } = useMouse()
</script>

<template>
  <p>Mouse position: {{ x }}, {{ y }}</p>
</template>

Why is this awesome?

  1. Logic Extraction: Your component implementation becomes cleaner.
  2. Reusability: Use useMouse in 50 different components.
  3. Organization: Group code by feature, not by option (data, methods, mounted).

Async Composable: useFetch

Let’s build something more practical. A wrapper around fetch.

composables/useFetch.ts

import { ref } from 'vue'

export function useFetch(url: string) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(true)

  fetch(url)
    .then(res => res.json())
    .then(json => (data.value = json))
    .catch(err => (error.value = err))
    .finally(() => (loading.value = false))

  return { data, error, loading }
}

Component

<script setup lang="ts">
import { useFetch } from './composables/useFetch'

const { data, loading, error } = useFetch('https://api.example.com/user')
</script>

<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">Oops! {{ error }}</div>
  <div v-else>
    <h1>{{ data.name }}</h1>
  </div>
</template>

Challenge for Day 9

  1. Create a composable useCounter(initialValue).
  2. It should return count, increment, and decrement.
  3. Use it in two different components.
  4. Observation: Does changing the count in Component A modify Component B? (Answer: No! Each call to useCounter creates fresh state).

Solution:

// composables/useCounter.ts
import { ref } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  function increment() { count.value++ }
  function decrement() { count.value-- }

  return { count, increment, decrement }
}

You have now mastered the Composition part of the Composition API. Tomorrow, we start building “Real” SPAs with Vue Router.