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.
Discover how at OpenReplay.com.
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 aredata
,loading
, anderror
, are created using Vue’sref
method and are initially set tonull
,false
, andnull
, respectively. - The
fetchData
function is asynchronous and it is responsible for making the fetch request. WhenfetchData
function is called or triggered, it setsloading
totrue
and after the fetch request is completed, the response is stored in thedata
variable, andloading
is set back tofalse
. If an error should occur at any point during the request, it will be in theerror
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
, anderror
, along with thefetchData
function. - The
onMounted
lifecycle hook, which is imported fromVue
, triggers thefetchData
function automatically when the component is mounted. This function fetches data from theurl
provided and updates the reactive variables,data
,loading
, anderror
, 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:
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.
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 thefetchData
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 thedata
,loading
, anderror
variables from theuseFetch
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,
Once again, when the fetch request is ongoing, you will see the loading user interface, as shown in the screenshot above.
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.