Skip to main content
Composables are reusable functions that leverage Vue’s Composition API to encapsulate and share stateful logic across components.

What are Composables?

Composables

Composables are JavaScript functions that:
  • Use Vue’s Composition API (ref, computed, watch, etc.)
  • Encapsulate reusable reactive logic
  • Can be shared across multiple components
  • Follow the use* naming convention

useGetData Composable

The useGetData composable handles HTTP requests using Axios:
src/composables/useGetData.js
import axios from 'axios'
import { ref } from 'vue'

export const useGetData = () => {
  const datos = ref(null); // Null because we don't know what we'll receive
  const cargando = ref(true);
  const error = ref(false);

  const getData = async (url) => {
    try {
      // Wait to get results from the API
      const resultado = await axios.get(url);
      datos.value = resultado.data; // Axios always returns .data
    }
    catch (err) {
      // Log error to console
      console.log(err);
      error.value = true;
    } 
    finally {
      cargando.value = false;
    }
  };
  
  return {
    getData,  // Return the function
    datos,    // Return the result obtained
    error,
    cargando
  }
};

Return Values

Type: (url: string) => Promise<void>Description: Async function that fetches data from the provided URLParameters:
  • url - API endpoint to fetch from
Side Effects:
  • Sets datos.value with response data
  • Sets error.value to true if request fails
  • Sets cargando.value to false when complete
Type: Ref<any | null>Description: Reactive reference containing the API response dataInitial Value: nullUsage:
<div v-if="datos">
  {{ datos.name }}
</div>
Type: Ref<boolean>Description: Reactive reference indicating if the request failedInitial Value: falseUsage:
<div v-if="error" class="error">
  Error loading data
</div>
Type: Ref<boolean>Description: Reactive reference indicating if the request is in progressInitial Value: trueUsage:
<div v-if="cargando" class="loading">
  Loading...
</div>

Usage in Components

Basic Usage

src/views/PokeView.vue
<script setup>
import { useRoute } from 'vue-router'
import { useGetData } from '@/composables/useGetData'

const route = useRoute()
const { getData, datos, error, cargando } = useGetData()

// Fetch Pokemon data when component mounts
getData(`https://pokeapi.co/api/v2/pokemon/${route.params.nombre}`)
</script>

<template>
  <div v-if="cargando" class="loading">
    <div class="spinner"></div>
    <p>Cargando información del Pokémon...</p>
  </div>

  <div v-else-if="error" class="error">
    <p>No se pudo cargar la información del Pokémon.</p>
  </div>   

  <div v-else class="pokemon-container">
    <h1>{{ datos.name }}</h1>
    <!-- Pokemon details -->
  </div>
</template>

Multiple API Calls

You can create multiple instances of the composable:
src/views/PokemonsView.vue
<script setup>
import { ref, onMounted } from 'vue'
import { useGetData } from '@/composables/useGetData'

const { getData, datos, error, cargando } = useGetData()
const offset = ref(0)
const limit = ref(20)

const fetchPokemons = () => {
  getData(`https://pokeapi.co/api/v2/pokemon?offset=${offset.value}&limit=${limit.value}`)
}

onMounted(() => {
  fetchPokemons()
})

const next = () => {
  offset.value += limit.value
  fetchPokemons()
}

const prev = () => {
  if (offset.value >= limit.value) {
    offset.value -= limit.value
    fetchPokemons()
  }
}
</script>

Reactive Fetching

Fetch data reactively when parameters change:
<script setup>
import { ref, watch } from 'vue'
import { useGetData } from '@/composables/useGetData'

const searchQuery = ref('')
const { getData, datos, error, cargando } = useGetData()

// Re-fetch when search query changes
watch(searchQuery, (newQuery) => {
  if (newQuery) {
    getData(`https://pokeapi.co/api/v2/pokemon/${newQuery.toLowerCase()}`)
  }
})
</script>

State Flow

The composable manages three states during the request lifecycle:
datos.value = null
cargando.value = true
error.value = false
// getData() called
cargando.value = true  // Still true
error.value = false    // Reset on new request
datos.value = { ...apiResponse }
cargando.value = false
error.value = false
datos.value = null  // Unchanged
cargando.value = false
error.value = true

Template Patterns

Common template patterns when using useGetData:

Loading, Error, Success

<template>
  <div v-if="cargando" class="loading">
    <div class="spinner"></div>
    <p>Cargando Pokémons...</p>
  </div>
  
  <div v-else-if="error" class="error">
    <p>Error al cargar los Pokémons.</p>
  </div>   
  
  <div v-else class="pokemon-grid">
    <div v-for="poke in datos.results" :key="poke.name">
      {{ poke.name }}
    </div>
  </div>
</template>

With Empty State

<template>
  <div v-if="cargando">Loading...</div>
  <div v-else-if="error">Error occurred</div>
  <div v-else-if="!datos || datos.length === 0">No results found</div>
  <div v-else>
    <!-- Display data -->
  </div>
</template>

Advantages of Composables

Benefits

Reusability: Same logic can be used in multiple componentsOrganization: Separates business logic from component templatesTestability: Composables can be tested independentlyType Safety: Works well with TypeScriptFlexibility: Easy to customize and extend

Best Practices

Show feedback to users while data is loading:
<div v-if="cargando" class="loading">
  <div class="spinner"></div>
  <p>Loading...</p>
</div>
Provide clear error messages when requests fail:
<div v-if="error" class="error">
  <p>Failed to load data. Please try again.</p>
  <button @click="retry">Retry</button>
</div>
Use optional chaining and nullish coalescing:
<template>
  <div>{{ datos?.name ?? 'Unknown' }}</div>
  <div v-if="datos?.results">
    <!-- Render results -->
  </div>
</template>
Consider resetting error state when making new requests:
const getData = async (url) => {
  error.value = false  // Reset error
  cargando.value = true
  try {
    const resultado = await axios.get(url)
    datos.value = resultado.data
  } catch (err) {
    error.value = true
  } finally {
    cargando.value = false
  }
}

Extending useGetData

You can extend or modify the composable for specific needs:

Adding Retry Logic

export const useGetData = () => {
  const datos = ref(null)
  const cargando = ref(true)
  const error = ref(false)
  const retryCount = ref(0)
  const maxRetries = 3

  const getData = async (url) => {
    try {
      const resultado = await axios.get(url)
      datos.value = resultado.data
      retryCount.value = 0 // Reset on success
    } catch (err) {
      console.log(err)
      error.value = true
      
      if (retryCount.value < maxRetries) {
        retryCount.value++
        setTimeout(() => getData(url), 1000 * retryCount.value)
      }
    } finally {
      cargando.value = false
    }
  }

  return { getData, datos, error, cargando, retryCount }
}

Adding Request Cancellation

import axios from 'axios'
import { ref } from 'vue'

export const useGetData = () => {
  const datos = ref(null)
  const cargando = ref(true)
  const error = ref(false)
  let cancelToken = null

  const getData = async (url) => {
    // Cancel previous request if exists
    if (cancelToken) {
      cancelToken.cancel('New request initiated')
    }
    
    cancelToken = axios.CancelToken.source()
    
    try {
      const resultado = await axios.get(url, {
        cancelToken: cancelToken.token
      })
      datos.value = resultado.data
    } catch (err) {
      if (!axios.isCancel(err)) {
        error.value = true
      }
    } finally {
      cargando.value = false
    }
  }

  return { getData, datos, error, cargando }
}

Next Steps

Project Structure

Learn about the codebase organization

State Management

Understand Pinia stores for global state

Build docs developers (and LLMs) love