← Back to all snippets
JAVASCRIPT

Crafting a Reusable useFetch Composable for API Calls in Vue 3

Build a robust, reusable `useFetch` composable in Vue 3 for handling asynchronous API requests, including loading states, error handling, and reactive data.

// composables/useFetch.js
import { ref, isRef, unref, watchEffect } from 'vue';

export function useFetch(url, options = {}) {
  const data = ref(null);
  const error = ref(null);
  const loading = ref(true);
  const controller = ref(null); // For aborting requests

  async function doFetch() {
    // Reset state before new fetch
    data.value = null;
    error.value = null;
    loading.value = true;

    // Abort previous request if it exists
    if (controller.value) {
      controller.value.abort();
    }
    controller.value = new AbortController();
    const signal = controller.value.signal;

    try {
      const fetchedUrl = isRef(url) ? url.value : url; // Handle reactive URLs
      const response = await fetch(fetchedUrl, { ...options, signal });
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      data.value = await response.json();
    } catch (e) {
      if (e.name === 'AbortError') {
        console.log('Fetch aborted.');
        // Do not set error if it was a deliberate abort
      } else {
        error.value = e;
      }
    } finally {
      loading.value = false;
    }
  }

  // watchEffect ensures fetch is re-executed if the URL or options change
  watchEffect(() => {
    // unref unwraps refs, so it works with both reactive and static URLs
    if (unref(url)) { // Only fetch if URL is not null/empty
      doFetch();
    }
  });

  // Return reactive states and a ref to the controller for manual abort if needed
  return { data, error, loading, abort: () => controller.value?.abort() };
}

// App.vue (Example usage)
<template>
  <div>
    <h1>User Data</h1>
    <p v-if="loading">Loading user...</p>
    <p v-else-if="error">Error: {{ error.message }}</p>
    <div v-else-if="data">
      <h2>{{ data.name }}</h2>
      <p>Email: {{ data.email }}</p>
      <p>Phone: {{ data.phone }}</p>
      <button @click="userId++">Next User (ID: {{ userId }})</button>
    </div>
    <p v-else>No user data.</p>
  </div>
</template>

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

const userId = ref(1);
const userUrl = computed(() => `https://jsonplaceholder.typicode.com/users/${userId.value}`);

// Pass a reactive URL to the composable
const { data, error, loading } = useFetch(userUrl);

// You could also use a static URL:
// const { data, error, loading } = useFetch('https://jsonplaceholder.typicode.com/todos/1');
</script>
How it works: The `useFetch` composable encapsulates the logic for making API requests, managing loading states, handling errors, and providing the fetched data reactively. It uses `ref` for data, error, and loading status, and `watchEffect` to automatically re-fetch data whenever the URL (or other reactive options) changes. It also incorporates `AbortController` for cancelling pending requests (preventing race conditions) and `isRef`/`unref` to gracefully handle both static and reactive `url` arguments, making it highly flexible and robust.

Need help integrating this into your project?

Our team of expert developers can help you build your custom application from scratch.

Hire DigitalCodeLabs