Back

Building a Custom Fetch Composable in Vue

Building a Custom Fetch Composable in Vue

Vue allows creating “composables” to extract and then reuse logic in multiple places of your app, and this article shows how to build a data fetching composable.

Vue.js is a popular JavaScript framework used to build user interfaces effectively and efficiently. One of its powerful features is the concept of composables. As a developer, you should always follow the “Don’t Repeat Yourself” (DRY) principle, and composables help you achieve this. Composables are functions that let you extract and reuse logic across different parts of your application. Instead of repeating the same lines of code in multiple places, you can create a composable function to handle the functionality and use it wherever you need. This method will keep your code more organized, easier to maintain, and scalable.

You might wonder about the difference between composable and regular functions.

Well, the key difference is that composables allow the use of reactive elements, while regular functions do not. Both composables and functions help with reusability, but composables work with Vue’s reactivity system. This means that, composables keep your data and user interface (UI) synchronized automatically, whereas regular functions do not have this reactive feature.

By the end of this article, you will understand how to build a reusable function for fetching data from an API, handle errors and loading states efficiently, and make your composable configurable for different scenarios.

Setting Up the Environment

You can follow along with the demonstration in this article using CodeSandbox. CodeSandbox allows you to run Vue projects directly in your browser without needing to install anything on your computer.

How Vue Composables Work: Understanding Reactivity and Function Structure

You need to understand how reactivity works in composables and how to structure a composable function properly for you to create applications that are dynamic, flexible, and easy to maintain.

How Reactive Data Works in Vue and Its Role in Composables

Vue uses reactivity to synchronize your application’s data with the user interface. This means that when your data changes, the user interface updates automatically. In composables, reactivity plays a major role because it allows you to create functions that return reactive data, which your components can then use.

Structure of a Composable Function

A typical composable function in Vue will have the following structure:

import { ref } from "vue";

export function useMyComposable() {
  const data = ref(null); // Creating reactive data

  const functionToUpdateReactiveData = async () => {
    try {
      data.value = "updated content"; // Updating reactive data
 } catch (error) {
      console.log(error); // Handling errors
 }
 };

// This is the object exposed to components calling the composable.
  return {
    data,
    functionToUpdateReactiveData,
 };
}

In this structure, ref is used to create reactive data variables. Remember that reactive means that the user interface updates automatically when your data changes. The composable returns an object with the reactive data and a function to update the reactive data. I will explain this structure in more detail as we build our composable.

How to Build a Custom Fetch Composable

Create a composables folder in your src directory to get started. Inside this folder, create a file named useFetch.js. Although you can choose a different name for your file and function, following the general naming convention with the use prefix helps differentiate between composable and regular functions.

Now, copy and paste the following code into the just-created file:

import { ref } from "vue";

export function useFetch(url) {
//  Reactive variables
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);

//  Function to perform a task and update the reactive variables appropriately.
  const fetchData = async (url) => {
    loading.value = true;
    try {
      const response = await fetch(url);
      data.value = await response.json();
 } catch (err) {
      error.value = err.message;
 } finally {
      loading.value = false;
 }
 };
    
// Returned/Exposed variables 
  return {
    data,
    loading,
    error,
    fetchData,
 };
}

Let me walk you through the code in the snippet above,

  • The useFetch composable is defined to accept a url parameter and return an object containing reactive variables and a function for fetching data and updating these variables. The reactive variables, which are data, loading, and error, are created using Vue’s ref method and are initially set to null, false, and null, respectively.
  • The fetchData function is asynchronous and it is responsible for making the fetch request. When fetchData function is called or triggered, it sets loading to true and after the fetch request is completed, the response is stored in the data variable, and loading is set back to false. If an error should occur at any point during the request, it will be in the error variable.
  • These reactive variables and the fetchData function are returned from the composable for use in your components.

Before we go ahead and use the composable, I want to point out that the current implementation is not very reusable, and this is because, If you need to use the fetchData function with different URLs, the current code doesn’t allow for that. You would have to initialize the composable multiple times for different URLs or endpoints, which defeats the purpose of using composables.

The issue is because the url is passed to the composable itself and not to the fetchData function. To resolve this, you should update the code to pass the url directly to the fetchData function. This change will bring about the following benefits,

  • Improve Flexibility: You can use different URLs with the same composable instance.
  • Enhance Performance Efficiency: It is only one instance of the composable that needs to be imported, regardless of how many URLs you want to work with.

To address the limitations and make the composable more flexible, update your code with the following:

import { ref } from "vue";

export function useFetch() {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);

  const fetchData = async (url) => {
    loading.value = true;
    try {
      const response = await fetch(url);
      data.value = await response.json();
 } catch (err) {
      error.value = err.message;
 } finally {
      loading.value = false;
 }
 };

  return {
    data,
    loading,
    error,
    fetchData,
 };
}

The lines of code in this snippet are almost the same as the one explained earlier. What changed is that the URL parameter has been moved from the useFetch composable to the fetchData function. So, instead of reinitializing the composable multiple times, one will just call the’ fetchData` function as many times as needed and pass the URL to it.

Now, let us go ahead and make use of the composable in our Vue component.

How to Use the Custom Fetch Composable in Vue Components?

To use the useFetch composable in a Vue component, you will take the following steps:

Navigate to your components folder, create a file named Users.vue, and add the following code to the file:

<script setup lang="ts">
import { onMounted } from "vue";
import { useFetch } from "../composables/useFetch.js";

// Destructuring the composable
const { data, loading, error, fetchData } = useFetch();

// Calling the fetchData function when the component is mounted
onMounted(async () => {
  await fetchData("https://dummyjson.com/users");
});
</script>

<template>
  <div class="users">
    <div v-if="loading" class="loading">Loading...</div>
    <div v-if="error" class="error">{{ error }}</div>
    <ul v-if="data && data.users" class="user-list">
      <li v-for="user in data.users" :key="user.id" class="user-item">
        <h3>{{ user.firstName }} {{ user.lastName }}</h3>
        <p>Email: {{ user.email }}</p>
      </li>
    </ul>
  </div>
</template>

<style scoped>
.users {
  width: 100%;
}
.loading {
  text-align: center;
  margin-top: 4rem;
  font-size: 2.5rem;
  color: #efeaea;
}

.error {
  color: red;
  text-align: center;
  font-size: 18px;
}
.user-list {
  width: 90%;
  display: flex;
  justify-content: space-between;
  list-style: none;
  flex-wrap: wrap;
}

.user-item {
  border: 1px solid #ddd;
  flex-basis: 30%;
  padding: 0.5rem;
  margin: 0.7rem 0;
}

.user-item h3 {
  margin: 0;
  font-size: 20px;
}

.user-item p {
  margin: 5px 0 0;
  color: #dbd5d5;
}
</style>

Let me walk you through what the code in the snippet above entails.

  • The useFetch composable is imported and initialized, and its returned object is destructured to access the reactive variables, data, loading, and error, along with the fetchData function.
  • The onMounted lifecycle hook, which is imported from Vue, triggers the fetchData function automatically when the component is mounted. This function fetches data from the url provided and updates the reactive variables, data, loading, and error, also available in the component for use in the template section.
  • In the template section, the user interface displayed depends on the state of these reactive variables. A loading indicator will be shown while data is being fetched, an error message will be displayed if an error occurs, and a list of users will be rendered when the data is successfully fetched.

Also, basic styling has been added to improve the look of the different UI.

To render the Users.vue component, you need to import and include it in your main App.vue file, as shown below.

<script setup lang="ts">
import Users from "./components/Users.vue";
</script>

<template>
  <main class="main">
    <header class="header">Dummy Users</header>
    <Users />
  </main>
</template>

<style scoped>
*,
*::after,
*::before {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}

.header {
  text-align: center;
  font-size: 2rem;
  font-weight: bold;
  padding: 1.5rem;
  background-color: #18b95e;
  border-bottom: 1px solid #bfbdbd;
}

.main {
  width: 100%;
  background: rgb(15, 41, 25);
  color: white;
  padding-bottom: 2rem;
}
</style>

Here, the Users.vue component is imported and rendered within the App.vue component, which is the entry point of the application since it is the component that is mounted.

After completing the above steps, you can go ahead and preview the application.

You will observe the following:

image

When the fetch request is ongoing, remember that loading will be set to true; therefore, you will see the loading user interface as shown in the screenshot above.

image

Once the request is completed, the user interface will change, and you will see the list of users fetched from the API, as shown in the screenshot above.

Making Your Composable More Flexible with Dynamic Configuration

Our current composable is limited to making only GET requests, as that is the default behavior of the fetch API. This limitation means it cannot be used for different HTTP methods like POST, PUT, or DELETE. As a developer, I aim to make composables as reusable as possible. Rewriting the same code repeatedly is not just tedious—it also increases your application size and makes maintenance harder.

Let us improve our composable for better flexibility.

To make the useFetch composable more reusable, update it so that the fetchData function accepts a configuration object instead of just a URL, as shown below:

import { ref } from "vue";

export function useFetch() {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);

  const fetchData = async (config) => {
    loading.value = true;
    try {
      const response = await fetch(config.url, {
        method: config.method || "GET", // Default to GET if no method is provided
        headers: config.headers || {},
        body: config.body ? JSON.stringify(config.body) : null, // Include body if provided
 });
      data.value = await response.json();
 } catch (err) {
      error.value = err.message;
 } finally {
      loading.value = false;
 }
 };

  return {
    data,
    loading,
    error,
    fetchData,
 };
}

With this update, everything remains the same except for the fetchData function, which now accepts a configuration object. While calling this function, the URL property must always be included in the object passed to the function. The headers and body properties are optional but required for methods such as POST, PUT, etc. The request will use GET method if no method is provided in the object and assume empty headers and body which are not needed for GET request.

Next, update your Users.vue component to use this new configuration:

<script setup lang="ts">
import { onMounted } from "vue";
import { useFetch } from "../composables/useFetch.js";

const { data, loading, error, fetchData } = useFetch();

onMounted(async () => {
  await fetchData({ url: "https://dummyjson.com/users" });
});
</script>

<template>
  <div class="users">
    <div v-if="loading" class="loading">Loading...</div>
    <div v-if="error" class="error">{{ error }}</div>
    <ul v-if="data && data.users" class="user-list">
      <li v-for="user in data.users" :key="user.id" class="user-item">
        <h3>{{ user.firstName }} {{ user.lastName }}</h3>
        <p>Email: {{ user.email }}</p>
      </li>
    </ul>
  </div>
</template>

<style scoped>
.users {
  width: 100%;
}
.loading {
  text-align: center;
  margin-top: 4rem;
  font-size: 2.5rem;
  color: #efeaea;
}

.error {
  color: red;
  text-align: center;
  font-size: 18px;
}
.user-list {
  width: 90%;
  display: flex;
  justify-content: space-between;
  list-style: none;
  flex-wrap: wrap;
}

.user-item {
  border: 1px solid #ddd;
  flex-basis: 30%;
  padding: 0.5rem;
  margin: 0.7rem 0;
}

.user-item h3 {
  margin: 0;
  font-size: 20px;
}

.user-item p {
  margin: 5px 0 0;
  color: #dbd5d5;
}
</style>

So instead of passing just a URL string to the fetchData function as we did with the earlier configuration, we now pass an object. The object includes the URL as a property. Remember that if you don’t specify a method, it will default to GET.

You will get the same result as before using this new setup.

Now let us demonstrate making a POST request. Create a new component called Login.vue in the components folder and import it into App.vue similar to Users.vue and paste the code below in the newly created Login.vue file.

<script setup lang="ts">
import { ref } from "vue";
import { useFetch } from "../composables/useFetch.js";

const username = ref("");
const password = ref("");
const { data, loading, error, fetchData } = useFetch();

const handleSubmit = async () => {
  await fetchData({
    url: "https://dummyjson.com/auth/login",
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: { username: username.value, password: password.value },
 });
};
</script>

<template>
  <div class="login-container">
    <form @submit.prevent="handleSubmit" class="login-form">
      <div class="form-group">
        <label for="username">Username:</label>
        <input type="text" id="username" v-model="username" required />
      </div>
      <div class="form-group">
        <label for="password">Password:</label>
        <input type="password" id="password" v-model="password" required />
      </div>
      <button type="submit" class="submit-button">Login</button>
    </form>
    <section>
      <div v-if="loading" class="loading">Loading...</div>
      <div v-if="error" class="error">{{ error }}</div>
      <div v-if="data" class="result">
        <h3>Login Successful!</h3>
        <p><strong>Username:</strong> {{ data.username }}</p>
        <p><strong>Email:</strong> {{ data.email }}</p>
      </div>
    </section>
  </div>
</template>

<style scoped>
.login-container {
  width: 80%;
  margin: 1.3rem auto 1rem;
  padding: 1rem;
  border: 1px solid #ddd;
  border-radius: 0.4rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.login-form {
  flex-basis: 45%;
  display: flex;
  flex-direction: column;
}

.form-group {
  width: 100%;
  margin-bottom: 1rem;
}

label {
  display: block;
  margin-bottom: 0.5rem;
}
input {
  width: 94%;
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 3px;
}
.submit-button {
  padding: 0.5rem;
  background-color: #18b95e;
  border: none;
  border-radius: 3px;
  color: white;
  font-size: 1rem;
  cursor: pointer;
}

.submit-button:hover {
  background-color: #16a750;
}

section {
  flex-basis: 45%;
}
.loading,
.error {
  text-align: center;
  margin-top: 1rem;
  font-size: 2.5rem;
}

.error {
  color: red;
}

.result {
  margin-top: 1rem;
  padding: 1rem;
  border: 1px solid #ddd;
  border-radius: 5px;
  background-color: #f9f9f9;
  color: black;
}

.result img.avatar {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  margin-top: 1rem;
}
</style>

So here is what is going on in the snippet above,

  • The ref function from Vue is used to create reactive variables for username and password, which are linked to the form inputs. As the user types, these variables update with the input values.
  • When the form is submitted, the handleSubmit function gets executed, and the fetchData function sends a POST request to the login API. The inputted username and password (e.g., ‘emilys’ and ‘emilyspass’ as detailed in the dummy JSON documentation) are sent as part of the request body, and the data, loading, and error variables from the useFetch composable are updated based on the response gotten from the API.
  • An object is passed to the fetchData function where the method, body, and headers are specified for the POST request. For POST requests, all of these properties are needed, but for a GET request, just the URL will suffice, based on how we configured the composable.
  • In the template, the interface changes based on these reactive variables. While the request is ongoing, a “Loading…” message is displayed. If an error occurs, it shows an error message. If the login is successful, the response data will be shown.

On clicking on submit, you will observe the following,

image

Once again, when the fetch request is ongoing, you will see the loading user interface, as shown in the screenshot above.

image

Once the request is completed, the user interface will change from loading UI to login successful UI as shown in the screenshot above.

We can see that our fetch composable can handle different HTTP methods and configurations.

Conclusion

In this article, we have seen how custom fetch composable makes handling API requests easier by being reusable and simple. We saw how to set up a fetch composable to handle different HTTP methods and configurations.

You can further improve the fetch composable by incorporating caching to improve performance. This will make your composable even more powerful and versatile.

You can also learn more about composables from the documentation.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay