Best Practices for Fetching Data in Vue.js

Axios is a popular Javascript library used to make HTTP requests from the browser or Node.js, it is an alternative to Javascripts Fetch API. Why axios? It is widely used in frontend frameworks

October 28, 20245 min read

Fetching data in any application is a common practice, in order to make our code more efficient and avoid common pitfalls while fetching data we need to follow some of the best practices out there. Here are some of best practices used out there by most Vue.js developers out in the industry.

Adding Axios to you project

Assuming you have already set up your project using you Vue CLI or your preferred method, we are going to add Axios to our project.

npm install axios

Axios is a popular Javascript library used to make HTTP requests from the browser or Node.js, it is an alternative to Javascripts Fetch API. Why axios? It is widely used in frontend frameworks such as Vue, React and Angular because of various reasons, I particularly love it because it has automatic JSON transformation reducing the need to manually parse and stringify data. It also provides consistent error handling and clear error messages making debugging easier.

Abstract Data Fetching to a Separate Service Layer

Separating API requests into service files makes your code more modular, reusable and easy to test. It can then be imported into any component simplifying updates to your API logic across components.

For example, create a userService.js to handle user-related API calls:

// userService.js
import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  withCredentials: false,
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json'
  }
});

export default {
  getUsers() {
    return apiClient.get('/users');
  }
};

Try Kodaschool for free

Click below to sign up and get access to free web, android and iOs challenges.

Sign Up

Use Composables for Reusable Logic.

Vue’s composition API allows you to create composable functions. Composable functions arefunctions that encapsulate and organize reusable logic. They are part of the Composition API and allow you to separate concerns by grouping related logic outside of components

Example:

We will create a composable for fetching data, the composable will be responsible for accepting a URL for fetching data from, manage loading and error states, return data so that any component can use it.

// src/composables/useFetch.js
import { ref } from 'vue';
import axios from 'axios';

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

  const fetchData = async () => {
    isLoading.value = true;
    error.value = null;

    try {
      const response = await axios.get(url);
      data.value = response.data;  // Axios automatically parses JSON
    } catch (err) {
      error.value = err.response ? err.response.data.message : err.message;
    } finally {
      isLoading.value = false;
    }
  };

  // Automatically fetch data when the composable is used
  fetchData();

  return {
    data,
    isLoading,
    error,
  };
}

Using the composable

<!-- src/components/PostsList.vue -->
<template>
  <div>
    <h1>Posts</h1>
    <div v-if="isLoading">Loading...</div>
    <div v-if="error">{{ error }}</div>
    <ul v-if="data && data.length">
      <li v-for="post in data" :key="post.id">{{ post.title }}</li>
    </ul>
  </div>
</template>

<script>
import { useFetch } from '@/composables/useFetch';

export default {
  name: 'PostsList',
  setup() {
    const { data, isLoading, error } = useFetch('https://jsonplaceholder.typicode.com/posts');
    
    return {
      data,
      isLoading,
      error,
    };
  },
};
</script>

Avoid Fetching Same Data in Multiple Components

Fetching data that is same in multiple components is unnecessary and can lead to issues such as redundant network requests potentially overloading the server and leading to increased network traffic. It also leads to inconsistent data states, due to the reason that components may fetch data at different times leading to inconsistent or out-of-sync states across the application.

Some of the work arounds to this are

  1. Global State Management (Vuex or Pinia)
  • A global store, like Vuex or Pinia, centralizes data and makes it accessible to any component, reducing the need to re-fetch it.
  1. Provide/Inject API
  • Use Vue’s provide/inject API to share data across nested components without passing props.
  1. Caching with Composables
  • You can use a composable function that caches data and serves it across multiple components. This approach works well if the data is relatively static.
  1. Parent-Child Data Flow
  • One simple strategy is to fetch data in a parent component and then pass it down to child components via props. This ensures that data is fetched only once and is shared across the necessary components.

Use Lifecycle Hooks Effectively

Mounted Hook: Use the mounted lifecycle hook for fetching data when a component loads.

Example

<template>
  <div v-if="isLoading">Loading...</div>
  <ul v-else>
    <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
  </ul>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      posts: [],        // To store the fetched posts
      isLoading: true,  // Loading state
      error: null,      // Error message if fetching fails
    };
  },
  mounted() {
    this.fetchPosts();  // Fetch posts when component mounts
  },
  methods: {
    async fetchPosts() {
      this.isLoading = true;  // Set loading to true
      try {
        const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
        this.posts = response.data;  // Store the fetched data
      } catch (error) {
        this.error = error.message;   // Set error message if fetching fails
      } finally {
        this.isLoading = false;  // Set loading to false
      }
    },
  },
};
</script>

Using beforeUnmount for Cleanup

The beforeUnmount hook is used to clean up any ongoing actions when the component is about to be removed from the DOM

Example

<template>
  <div v-if="isLoading">Loading...</div>
  <ul v-else>
    <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
  </ul>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      posts: [],
      isLoading: true,
      cancelTokenSource: null, // To store Axios CancelToken source
    };
  },
  mounted() {
    this.fetchPosts();  // Fetch posts when the component mounts
  },
  beforeUnmount() {
    // Cancel ongoing request if it exists
    if (this.cancelTokenSource) {
      this.cancelTokenSource.cancel('Request canceled by user.'); // Cancel the request
    }
  },
  methods: {
    async fetchPosts() {
      this.isLoading = true;
      this.cancelTokenSource = axios.CancelToken.source(); // Create CancelToken source
      try {
        const response = await axios.get('https://jsonplaceholder.typicode.com/posts', {
          cancelToken: this.cancelTokenSource.token, // Pass the cancel token
        });
        this.posts = response.data; // Store the fetched data
      } catch (error) {
        if (axios.isCancel(error)) {
          console.log('Request canceled', error.message); // Log cancellation
        } else {
          this.error = error.message; // Set error message if fetching fails
        }
      } finally {
        this.isLoading = false; // Set loading to false
      }
    },
  },
};
</script>

Implement Caching and Throttling

Caching

This is the process of storing frequently accessed data so that subsequent requests for the same data can be served quickly from memory rather than having to make a new request to the serve. It reduces load times and server load.

Manual Caching with Pinia

// stores/posts.js
import { defineStore } from 'pinia';
import axios from 'axios';

export const usePostsStore = defineStore('posts', {
  state: () => ({
    posts: [],
    isLoading: false,
    error: null,
  }),
  actions: {
    async fetchPosts() {
      // If posts are already cached, return early
      if (this.posts.length) {
        return;
      }
      this.isLoading = true;
      try {
        const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
        this.posts = response.data; // Cache the posts in the state
      } catch (error) {
        this.error = error.message; // Store error message
      } finally {
        this.isLoading = false; // Set loading to false
      }
    },
  },
});

Using the Store in a Component:

<template>
  <div v-if="isLoading">Loading...</div>
  <ul v-else>
    <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
  </ul>
</template>

<script>
import { usePostsStore } from '@/stores/posts';

export default {
  setup() {
    const store = usePostsStore();
    store.fetchPosts(); // Fetch posts when the component mounts

    return {
      posts: store.posts,
      isLoading: store.isLoading,
    };
  },
};
</script>

Throttling

Throttling is the limiting of the number of requests to a server at a particular time frame. It is particularly useful when dealing with user input, such as search fields, where you want to limit how often API calls are made as the user types.

Implementing Throttling with Axios

You can implement throttling using a debounce function. Here’s a simple example using a custom debounce function along with Axios to fetch data as the user types in a search input.

Debounce Function:

function debounce(func, delay) {
  let timeout;
  return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), delay);
  };
}

Using Debounce in a Component:

<template>
  <input v-model="search" placeholder="Search posts..." />
  <ul>
    <li v-for="post in filteredPosts" :key="post.id">{{ post.title }}</li>
  </ul>
</template>

<script>
import axios from 'axios';
import { ref, watch } from 'vue';
import { usePostsStore } from '@/stores/posts';

export default {
  setup() {
    const postsStore = usePostsStore();
    postsStore.fetchPosts(); // Fetch all posts initially

    const search = ref('');
    const filteredPosts = ref([]);

    // Debounce the search function
    const debouncedFetch = debounce(async (query) => {
      if (!query) {
        filteredPosts.value = postsStore.posts; // Show all posts if no search query
        return;
      }
      // Implement API call to search posts
      const response = await axios.get(`https://jsonplaceholder.typicode.com/posts?search=${query}`);
      filteredPosts.value = response.data; // Update filtered posts
    }, 300); // 300 ms debounce

    // Watch for changes in the search input
    watch(search, (newValue) => {
      debouncedFetch(newValue);
    });

    return {
      search,
      filteredPosts,
    };
  },
};
</script>

Conclusion

The following are some of the best practices while fetching data in Vue, there are a lot more out there, at least this are the once I have tried and tested.

Peace!

Jackson Obere

About Jackson Obere

I enjoy working with front-end technologies like Vue, React, and Vanilla JavaScript, with some Python on the side. I love building interactive web experiences and breaking down tricky concepts to make them easier to understand. I'm always curious and enjoy learning new things in the ever-evolving tech space.