Nuxt3: How We Eliminated 10 Extra Client API Requests Using $fetch and Pinia

Thursday, July 4, 2024


The below image shows how decreased the number of client requests from 11 to only one

nuxt-avoid-client-requests

Last year, we upgraded our Nuxt2 app to Nuxt3, which was challenging but worthwhile. Unfortunately, we couldn't fully utilize all the new Nuxt3 features like useFetch because migrating our app from "Axios/Vuex" to "useFetch" or different approaches was time-consuming given our large app.

Instead, we went with a simpler approach by using "$fetch/Pinia" something similar to "Axios/Vuex" approach, in order to make the transition process safer and quicker.



Our Fethcing Data Scenarios (for initial load requests)

1) Using "$fetch" to fetch data and storing it in Pinia Stores

The current way that we're using to handle API requests in our Nuxt3 app is having a wrapper Nuxt plugin called $API that internally uses the Nuxt $fetch for making requests, something like this:

stores/data.ts
        async function fetchData() {
  const { $API } = useNuxtApp();
  return await $API.get("/content/data").then(async response => {
    this.myData = response.data; // myData is a state property
  });
}

    

In our vue page, we call it like this:

pages/home.vue
        <template>{{ myData }}</template>

<script setup lang="ts">
const { fetchData, myData } = useContentStore();
await fetchData();
</script>

    

The problem

Using $fetch causes two requests: one on the server and one on the client. This is by design, as $fetch doesn't transfer data from the server to the client, so it needs to make two requests to ensure the Vue component's data is retrieved again.

Ref: Because $fetch does not transfer state from the server to the client. Thus, the fetch will be executed on both sides because the client has to get the data again.

This means if you hit reload on the home page, you will see a network request in the devtools for the /content/data API which is extra, as the same exact one happened on the server.

This is expected to have two requests when you use $fetch and store data in your Vue component, but it's an issue if you use $fetch to store data in a Pinia store? because Pinia state already preserved and transferred from the server to the client (state got hydrated), so that network request is considered an extra one.

The Solution

callOnce was especially introduced to solve such an issue as it was designed to execute a given function or block of code only a single time it executes once on server, and on client side navigation. Ref

We need to wrap our function call with callOnce:

pages/home.vue
        <!-- myData is a state propert in our content store -->
<template>{{ myData }}</template>

<script setup lang="ts">
const { fetchData, myData } = useContentStore();
await callOnce(async () => {
  await fetchData();
});
</script>

    

Now hit a reload, and watch your network tab, you will no longer see that request. 🔥🔥

This solution fits us, as we needed something quick and works in the same time, in the meantime, we will be transitioning gradually to other solutions like the below ones.

2) Using $fetch to fetch data and storing it in a Vue component

Not all APIs results are stored in Pinia stores, in some cases we store the data in Vue component' instance like below:

stores/data.ts
        async function fetchData() {
  const { $API } = useNuxtApp();
  return await $API.get("/content/data");
}

    

Getting the data and storing it in the component:

home.vue
        <template>{{ myData }}</template>

<script setup lang="ts">
const myData = ref();
const { fetchData } = useContentStore();
const res = await fetchData();
myData.value = res.data;
</script>

    

The problem

We don't store all the APIs results in Pinia stores, sometimes we store them in the same Vue component's instance. We used our plugin $API that uses $fetch internally and as expected it got executed twice .. we tried to wrap it with callOnce like below:

home.vue
        <template>{{ myData }}</template>
<script setup lang="ts">
const myData = ref();
const { fetchData } = useContentStore();
await callOnce(() => {
  const res = await fetchData();
  myData.value = res.data;
});
</script>

    

Yes, the above block of code will get executed once, but the data myData will be lost from the component when Nuxt hydrates the component.

Solution:

We can use useFetch to handle such a case.

The useFetch is a great comosable for handeling APIs easily, you can review it here or on Youtube By Alexander Lichter

In our real app we created a wrapper called useBaseFetch so we do it in common things like passing headers, token other stuff. We won't address this point, but if you're intersted check this example

useFetch is not meant to be used in pinia stores, mainly it should be called in inside <script setup></script> where it can automaticlly stores the API response data into your Vue component, and it makes sure the component's data is transferred from server to client.

Example of useFetch:

pages/home.vue
        <template>{{ myData }}</template>

<script setup>
const { data: myData } = await useFetch("/content/data");
</script>

    

Now check your network tab, you won't see the client API request on reload.

Note: the above methods are just the ways how we eliminated extra client requests when we were using $fetch and Pinia, its not about recommending an approach over other approaches for fetching data.

Main Benefits of the above enhancments

  1. Saving network requests means having better performance
  2. Eliminating expensive SQL queries that were caused by the extra client APIs requests