Search code examples
javascriptvue.jsnuxt.jsvite

Why is Vue's Composable not Isolated?


I'm using Vue 3 with Nuxt 3 and Vite, and I'm encountering a strange bug. It occurs almost everywhere in my app whenever I try to use useFetch within a composable.

For example, I have the following composable to check whether a button can be rendered based on the user's role:

useCheckerRole Composable

export function useCheckerRole(policies){
  const {data, error} = await useFetch("/api/role-checker", {
    method: "POST",
    body: policies
  })

  if(data.value) {
    return data.value
  }

  if(error.value) {
    throw new Error("an error occured");
  }
}

I then need to pass the policies like so whenever I need to check their roles.

const policies = await useCheckerRole([
  {
   component_ask: 'some-button',
   role_ask: 'administrator'
  }
])

which then returns something like this from the api:

  [{
   component_ask: 'some-button',
   role_ask: 'administrator',
   status: 'approved'
  }]

The Bug

The bug shows up when I mount useCheckerRole in two different components on the same page.

For instance, in my topbar component, I use the useCheckerRole to determine if a specific button or menu should be displayed, depending on the user's role. Meanwhile, elsewhere on the page, I have another button that requires similar role-based checking.

TopBar.vue

The topbar sends this to the role checker and then immediately logs the result

const policies = await useCheckerRole([
  {
   component_ask: 'top-bar-button',
   role_ask: 'administrator'
  }
]).catch((error)....)

console.log("topBar Policies result", policies)

ChildPage.vue

The child page sends this to the useCheckerRole and immediately logs the result

const policies = await useCheckerRole([
  {
   component_ask: 'child-page-button',
   role_ask: 'administrator'
  },
  {
   component_ask: 'child-page-other-button',
   role_ask: 'administrator'
  },
]).catch((error)....)

console.log("Child Policies result", policies)

TopBar.vue Result

now the result of the Topbar is interestingly this:

🐞 What TopBar got

[
  {
   component_ask: 'child-page-button',
   role_ask: 'administrator',
   status: "approved"
  },
  {
   component_ask: 'child-page-other-button',
   role_ask: 'administrator',
   status: "rejected"
  },
]

Notice that this was not what the Topbar component sent to the useCheckerRole, rather this was what the ChildPage sent.

✅ What TopBar should've gotten

[
  {
   component_ask: 'top-bar-button',
   role_ask: 'administrator',
   status: "approved"
  },
]

The ChildPage also got the same result as the Topbar:

[
  {
   component_ask: 'child-page-button',
   role_ask: 'administrator',
   status: "approved"
  },
  {
   component_ask: 'child-page-other-button',
   role_ask: 'administrator',
   status: "rejected"
  },
]

This made me realize that composables are not as isolated as I thought when using useFetch, or maybe I'm doing something very wrong. Because it looks like both component were calling to the same unisolated function at the same time even if said function are supposed to be isolated.

The initial result in TopBar was correct. However, when ChildPage also called the same function, it somehow overrode the result, which unfortunately wasn't what TopBar asked for.

Does anyone else have this issue?


Solution

  • After playing left and right with the solutions that Yue JIN and Estus Flask gave, I finally found the problem. I stumbled across this article - Nuxt 3 useFetch — Are you using it correctly? and read the following statement from the author:

    Generally as a rule guiding composables, they must be called only in; a setup function, Plugins, & Route Middleware. Trying to use this composable outside this scope will lead to bugs that affects performance of the app.

    Within the article, he demonstrated how the bug occured after wrapping useFetch within a function. Immediately after seeing this, I saw that my function was indeed wrapping a composable that's wrapping the useFetch composable.

    🐞The Code that Lead to Bug

    TopBar/ChildPage Component

    
    <script setup>
    const policy_list = ref([...])
    
    async function checkRole(){
     const policies = await useCheckerRole(policy_list .value).catch((error)=> 
       {...}
     )
    }
    
    checkRole();
    
    </script>
    
    

    that useCheckerRole is a composable within a function that uses useFetch. So the weird bug indeed appeared.

    ✅ My Solution

    lib/roleChecker.ts

    I decided to test this by creating a normal function and placed it outside of the composables folder; in a folder called lib.

    Inside of the roleChecker.ts, I no longer use useFetch. Instead, I use $fetch to call my api and return them.

    export default function roleChecker(
      url: string,
      options: { [key: string]: any },
      source?: string
    ) {
      return new Promise<any>(async (resolve, reject) => {
        console.log("roleChecker initialized");
    
        $fetch(url, options)
          .then((res) => {
            resolve(res);
          })
          .catch((error) => {
            reject(error);
          });
      });
    }
    

    Now for each of my component I can finally call the roleChecker within any function and the bugs no longer bother me.

    TopBar/ChildPage

    
    <script setup>
    import roleChecker from "/lib/roleChecker"
    const policy_list = ref([...])
    
    async function checkRole(){
     const policies = await roleChecker(policy_list .value).catch((error)=> 
       {...}
     )
    }
    
    checkRole();
    
    </script>