最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

vue.js - Nuxt 3 Custom $fetch with Token Refresh Not Updating Pinia State in Components - Stack Overflow

programmeradmin3浏览0评论

I'm building a Nuxt 3 app where I have a custom $fetch plugin to handle API requests, refresh expired tokens, and retry failed requests.

The fetch and retry mechanism work fine, but the state in my Pinia store and components does not update after a request is retried with a new token.

[1] In my component "transactions.vue" i am fetching the data on component creation

// fetch onCreate
transactionsStore.fetchTransactionsList({
  page: 1,
});

[2] In my Pina Store i have this action to fetch the transactions and load them in the "transactions.vue"

    async fetchTransactionsList(query) {
      const api = useApiRepository();

      const { status, data, error } = await useAsyncData(
        "transactions_list",
        () => api.getTransactionsList({
          ...query,
          per_page: 20,
        }),
      );

      this.transactionsData = data.value?.data ?? {};
      this.transactionsStatus = status;
      this.transactionsError = error;

      return { status, data, error };
    },

[3] I have a nuxt plugin to customize the default $fetch

import { defineNuxtPlugin } from "#app";
import { useLocalStorage } from "@vueuse/core";
import { useAuthStore } from "~/stores/AuthStore";

let api;



// Create a list to hold the Unauthorized 401 request queue
const refreshAndRetryQueue = [];

// Flag to prevent multiple token refresh requests
let isRefreshingToken = false;

// Counter for tracking the number of consecutive refresh attempts
let refreshAttempts = 0;
// Maximum number of consecutive refresh attempts allowed in Single
//  Session {{ Without Refreshing the page }}
const MAX_REFRESH_ATTEMPTS = 25;

// Main function to handle response errors
function handleResponseError(nuxtApp, authStore) {
  return async ({ request: requestURL, options, response }) => {
  // Check if the current route is an auth route
    const isAuthenticated = authStore?.isAuthenticated;

    // Handle Unauthorized 401 error when the user is already logged-in
    if (response.status === 401 && isAuthenticated) {
    // If we're not already refreshing the token
      if (!isRefreshingToken) {
        isRefreshingToken = true;

        // Increment the refresh attempts counter
        refreshAttempts++;

        // Check if we've exceeded the maximum number of refresh attempts
        if (refreshAttempts > MAX_REFRESH_ATTEMPTS) {
        // Too many refresh attempts, force logout
          handleLogout(nuxtApp, authStore);
          return;
        }

        // Call Refresh API and store the new token in the localStorage
        const refreshResponse = await authStore.requestNewAccessToken();

        if (refreshResponse.success) {
        // Refresh successful. Retry all requests in the queue with the new token
          refreshAndRetryQueue.forEach(({ requestURL, options }) => {
            api(requestURL, {
              ...options,
            });
          });

          // Clear the queue after retrying all requests
          refreshAndRetryQueue.length = 0;

          // Reset the refreshing flag
          isRefreshingToken = false;

          // Retry the original request that triggered the refresh
          return api(requestURL, {
            ...options,
          });
        }
        else {
        // Refresh failed, log out the user
          handleLogout(nuxtApp, authStore);
        }

        // Reset the refreshing flag
        isRefreshingToken = false;
      }

      // If we're already refreshing (isRefreshingToken = true),
      //  add the request to the queue to be called after we finish the refreshing
      return refreshAndRetryQueue.push({
        requestURL,
        options,
      });
    }
  };
}

// Function to handle user logout
async function handleLogout(nuxtApp, authStore) {
  // Clear local authentication data
  authStore.clearLocalAuthData();
  // Redirect to session-expired page
  await nuxtApp.runWithContext(() => navigateTo("/auth/session-expired"));

  // Reset States on Logout
  refreshAttempts = 0;
  isRefreshingToken = false;
}

// Main plugin function
export default defineNuxtPlugin((nuxtApp) => {
  const config = useRuntimeConfig();
  const authStore = useAuthStore();

  // Create a custom instance of ofetch with global configurations
  api = $fetch.create({
    baseURL: config.public.apiBaseUrl,

    // Add hooks for request and response
    onRequest: handleRequest, //Already implemented but the function is not added here for simplicity
    onResponseError: handleResponseError(nuxtApp, authStore),
  });

  // Provide the custom fetch instance to the app
  return {
    provide: {
      api, // This can be used in the app as $api
    },
  };
});


The Issue:

I call transactionsStore.fetchTransactionsList({ page: 1 }) in my transactions.vue component. The fetchTransactionsList action in my Pinia store (transactionsStore.js) uses useAsyncData to fetch transactions and update state. When a 401 Unauthorized error occurs, the plugin refreshes the token, retries the request, and successfully fetches the data. However, the new data does not reflect in my components, not in the Pinia states also

I'm building a Nuxt 3 app where I have a custom $fetch plugin to handle API requests, refresh expired tokens, and retry failed requests.

The fetch and retry mechanism work fine, but the state in my Pinia store and components does not update after a request is retried with a new token.

[1] In my component "transactions.vue" i am fetching the data on component creation

// fetch onCreate
transactionsStore.fetchTransactionsList({
  page: 1,
});

[2] In my Pina Store i have this action to fetch the transactions and load them in the "transactions.vue"

    async fetchTransactionsList(query) {
      const api = useApiRepository();

      const { status, data, error } = await useAsyncData(
        "transactions_list",
        () => api.getTransactionsList({
          ...query,
          per_page: 20,
        }),
      );

      this.transactionsData = data.value?.data ?? {};
      this.transactionsStatus = status;
      this.transactionsError = error;

      return { status, data, error };
    },

[3] I have a nuxt plugin to customize the default $fetch

import { defineNuxtPlugin } from "#app";
import { useLocalStorage } from "@vueuse/core";
import { useAuthStore } from "~/stores/AuthStore";

let api;



// Create a list to hold the Unauthorized 401 request queue
const refreshAndRetryQueue = [];

// Flag to prevent multiple token refresh requests
let isRefreshingToken = false;

// Counter for tracking the number of consecutive refresh attempts
let refreshAttempts = 0;
// Maximum number of consecutive refresh attempts allowed in Single
//  Session {{ Without Refreshing the page }}
const MAX_REFRESH_ATTEMPTS = 25;

// Main function to handle response errors
function handleResponseError(nuxtApp, authStore) {
  return async ({ request: requestURL, options, response }) => {
  // Check if the current route is an auth route
    const isAuthenticated = authStore?.isAuthenticated;

    // Handle Unauthorized 401 error when the user is already logged-in
    if (response.status === 401 && isAuthenticated) {
    // If we're not already refreshing the token
      if (!isRefreshingToken) {
        isRefreshingToken = true;

        // Increment the refresh attempts counter
        refreshAttempts++;

        // Check if we've exceeded the maximum number of refresh attempts
        if (refreshAttempts > MAX_REFRESH_ATTEMPTS) {
        // Too many refresh attempts, force logout
          handleLogout(nuxtApp, authStore);
          return;
        }

        // Call Refresh API and store the new token in the localStorage
        const refreshResponse = await authStore.requestNewAccessToken();

        if (refreshResponse.success) {
        // Refresh successful. Retry all requests in the queue with the new token
          refreshAndRetryQueue.forEach(({ requestURL, options }) => {
            api(requestURL, {
              ...options,
            });
          });

          // Clear the queue after retrying all requests
          refreshAndRetryQueue.length = 0;

          // Reset the refreshing flag
          isRefreshingToken = false;

          // Retry the original request that triggered the refresh
          return api(requestURL, {
            ...options,
          });
        }
        else {
        // Refresh failed, log out the user
          handleLogout(nuxtApp, authStore);
        }

        // Reset the refreshing flag
        isRefreshingToken = false;
      }

      // If we're already refreshing (isRefreshingToken = true),
      //  add the request to the queue to be called after we finish the refreshing
      return refreshAndRetryQueue.push({
        requestURL,
        options,
      });
    }
  };
}

// Function to handle user logout
async function handleLogout(nuxtApp, authStore) {
  // Clear local authentication data
  authStore.clearLocalAuthData();
  // Redirect to session-expired page
  await nuxtApp.runWithContext(() => navigateTo("/auth/session-expired"));

  // Reset States on Logout
  refreshAttempts = 0;
  isRefreshingToken = false;
}

// Main plugin function
export default defineNuxtPlugin((nuxtApp) => {
  const config = useRuntimeConfig();
  const authStore = useAuthStore();

  // Create a custom instance of ofetch with global configurations
  api = $fetch.create({
    baseURL: config.public.apiBaseUrl,

    // Add hooks for request and response
    onRequest: handleRequest, //Already implemented but the function is not added here for simplicity
    onResponseError: handleResponseError(nuxtApp, authStore),
  });

  // Provide the custom fetch instance to the app
  return {
    provide: {
      api, // This can be used in the app as $api
    },
  };
});


The Issue:

I call transactionsStore.fetchTransactionsList({ page: 1 }) in my transactions.vue component. The fetchTransactionsList action in my Pinia store (transactionsStore.js) uses useAsyncData to fetch transactions and update state. When a 401 Unauthorized error occurs, the plugin refreshes the token, retries the request, and successfully fetches the data. However, the new data does not reflect in my components, not in the Pinia states also

Share Improve this question edited Feb 7 at 13:23 Zayed asked Feb 7 at 13:18 ZayedZayed 133 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

I have a guess in what is wrong with your code.

Using useAsyncData inside a Pinia store is not very good because it's designed for page-level data fetching and comes with built-in caching and reactivity behaviors that can interfere with how you manage state in a store. When you use useAsyncData, especially with a static key, the returned data might be cached, meaning that even after a token refresh or a retried request, the store and components may not reflect the latest data.

By using plain async/await within your store, you have full control over the asynchronous flow, error handling, and state updates. This approach avoids the caching issues and allows the store to update immediately once the new data is retrieved, ensuring that your components always display the latest information.

I'd change the code for something like this:

async fetchTransactionsList(query) {
  const api = useApiRepository();
  try {
    // Make the API call using your custom $fetch instance
    const response = await api.getTransactionsList({
      ...query,
      per_page: 20,
    });

    // Update the store with the retrieved data.
    // Adjust the property names based on your API response structure.
    this.transactionsData = response.data?.data || {};
    this.transactionsStatus = response.status || 200;
    this.transactionsError = null;

    return response;
  } catch (error) {
    // In case of an error, update the store accordingly.
    this.transactionsData = {};
    this.transactionsStatus = error.response?.status || error.status || 500;
    this.transactionsError = error;

    // Optionally, re-throw the error so the calling component is aware of the failure.
    throw error;
  }
}

By doing so, you prevent any side effects that useAsyncData might be causing in your store.

发布评论

评论列表(0)

  1. 暂无评论