atinux / nuxt-auth-utils

Add Authentication to Nuxt applications with secured & sealed cookies sessions.
MIT License
973 stars 91 forks source link

Nuxt Instance error on try to integrate Nuxt Auth and TanStack. #273

Closed guilherme-codes closed 1 week ago

guilherme-codes commented 1 week ago

I'm using TanStack to fetch and cache my data, so I created a plugin to manage it. After three retries I'd like to redirect the user to the auth path and clear the session. The problem is that I'm receiving the famous "A composable that requires access to the Nuxt instance was called outside of a plugin..." error.

I tried to use nuxtApp.runWithContext(() => {}) to manage it but unfortunately doesn't work.

Is there any way to fix it @atinux?? I'll place my code below to make it easy to understand.

import type {
  DehydratedState,
  VueQueryPluginOptions,
} from '@tanstack/vue-query';

import {
  VueQueryPlugin,
  QueryClient,
  hydrate,
  dehydrate,
  QueryCache,
} from '@tanstack/vue-query';

import { useState } from '#app';

export default defineNuxtPlugin((nuxt) => {
  const { clear } = useUserSession();

  const config = useRuntimeConfig();
  const vueQueryState = useState<DehydratedState | null>('vue-query');
  const nuxtApp = useNuxtApp();

  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 3000,
        retry: (failureCount, error) => {
          if (failureCount < 3) {
            return true;
          }

          redirectIfUnauthorized(error);
          return false;
        },
      },
    },
    queryCache: new QueryCache({
      onError: (error) => redirectIfUnauthorized(error),
    }),
  });

  const options: VueQueryPluginOptions = { queryClient };

  nuxt.vueApp.use(VueQueryPlugin, options);

  if (import.meta.server) {
    nuxt.hooks.hook('app:rendered', () => {
      vueQueryState.value = dehydrate(queryClient);
    });
  }

  if (import.meta.client) {
    nuxt.hooks.hook('app:created', () => {
      hydrate(queryClient, vueQueryState.value);
    });
  }

  const redirectIfUnauthorized = (error: unknown) => {
    const response = error
    const authPath = config.public.authPath as string;

    if (response?.status === 401) {
      nuxtApp.runWithContext(() => {
        navigateTo(authPath, { external: true })
        clear();  // it is throwing the error
      })
    }
  };

});
atinux commented 1 week ago

Hi @guilherme-codes

Can you please try with this version and confirm that the fix works?

pnpm add https://pkg.pr.new/atinux/nuxt-auth-utils@278

Then start again your development server and try to reproduce your issue.

guilherme-codes commented 1 week ago

It works well. Thanks

atinux commented 1 week ago

You can now use https://github.com/atinux/nuxt-auth-utils/releases/tag/v0.5.3 :)

sandros94 commented 1 week ago

@atinux I'm curious (on a module author's perspective) to understand something in your fix (https://github.com/atinux/nuxt-auth-utils/pull/278/commits/065c54dba8e2b06b356de421bca4a91881158a4f)

What was causing it? Was it the fact that fetch and clear where defined outside useUserSession?

atinux commented 1 week ago

The main issue was calling useSessionState().value = {} after an await, which itself use useState() that depends on the current instance (see here)

The fix was to use the variables defined before any await in order to work properly.