auth0/auth0-vue

Ensure the user is available outside of a component?

john-goldsmith opened this issue · 4 comments

Checklist

  • I have looked into the Readme, Examples, and FAQ and have not found a suitable solution or answer.
  • I have looked into the API documentation and have not found a suitable solution or answer.
  • I have searched the issues and have not found a suitable solution or answer.
  • I have searched the Auth0 Community forums and have not found a suitable solution or answer.
  • I agree to the terms within the Auth0 Code of Conduct.

Describe the problem you'd like to have solved

Hello,

Thanks for taking the time and effort to author this package.

My question: what is the best way to ensure the user is loaded and available when the plugin is used outside the context of a component?

The use case I'm trying to understand and solve for: I use the Segment telemetry service that needs to "identify" a user so that all subsequent events are tied to that user. The issue I'm facing is that the user object exposed by the plugin is reactive, and thus, not guaranteed to be available without some sort of change detection mechanism (aka, watch).

Issue #99 is the closest issue I've found related to this, but despite trying my best to parse through it, I'm not sure the use case is quite the same.

An abridged version:

// src/plugins/auth0.ts
import { createAuth0 } from '@auth0/auth0-vue'
export default createAuth0({...})
// src/composables/useSegment.ts
import { watch } from 'vue'
import auth0Plugin from '@/src/plugins/auth0.ts'
import { AnalyticsBrowser } from '@segment/analytics-next'

const segment = new AnalyticsBrowser()

export function useSegment() {
  /**
   * The following only ever needs to happen once,
   * and the user object must be available. It seems
   * problematic to put the watcher here because
   * anytime useSegment is invoked, it will unnecessarily
   * create a watcher.
   * 
   * Is this fine as-is and expected to be watched
   * outside of components?
   */
  watch(auth0Plugin.user, () => {
    if (auth0Plugin.user.value) {
      segment.identify({ email: auth0Plugin.user.value?.email })
      segment.load() // sends off buffered events
    }
  })

  return {
    track: segment.track
  }
}
// src/components/HelloWorld.vue
import { useSegment } from '@/composables/useSegment'

<script setup lang="ts">
const segment = useSegment()
</script>

<template>
  <button @click="segment.track({...})">Submit</button>
</template>

Is there some way to await the user? Is auth0-spa-js somehow better suited for this use case? Is there a more "component-first" approach? I'm open to any ideas or alternatives. Thanks for any insight you can provide!

Describe the ideal solution

I'll admit that I'm not very familiar with the inner workings of Auth0, so this might not even make sense or be very Vue3-like. Perhaps the plugin could expose a way to await getting the user. Pseudo-code:

// src/composables/whatever.ts
const { getUser, isAuthenticated, isLoading } = useAuth0()

if (isAuthenticated && !isLoading) {
  const user = await getUser()
}

That begs a similar question, however, of reacting to changes in isAuthenticated and isLoading.

Alternatives and current workarounds

No response

Additional context

No response

Our SDK exposes a isLoading property. The meaning of this property is explained here.

In general, this means: Do not interact with the SDK until isLoading is false. So what I think you want is:

  • Somewhere at the top level, ensure the part that needs the user information is not rendered unless isLoading is false.
  • Doing so, you can guarantee that whenever useSegment is executed, the SDK is done loading. This means the user is "available", so you can just read its value without a watcher. I say "available", as the user can still be null which indicates the user is not logged in. If you want to ensure useSegment is only ever executed when there is an actual user, ensure you do the same thing with isAuthenticated (ensure the part of the DOM that needs useSegment is not rendered unless isLoading is false, and isAuithenticated s true).

The above should allow you to integrate it without a watcher.

Closing, as I think this has been answered. Can always reopen if needed.

Thanks for the reply. My understanding of isLoading and isAuthenticated is inline with your descriptions, so that part makes sense. I'm not quite understanding what you mean by the following, however:

...ensure the part that needs the user information is not rendered unless isLoading is false

The part that needs the user information is useSegment() itself. There is no rendering, in the direct sense of the word -- useSegment is a composable that gets used by practically every component. Are you suggesting some sort of wrapper component that prevents all subsequent components from rendering until the user is available?

<Segment>
  <App>
    <HelloWorld />
  </App>
</Segment>
// @/src/components/Segment.vue
<script setup>
const { useAuth0 } from '@auth0/auth0-vue'
const { useSegment } from '@/composables/useSegment'

const auth0 = useAuth0()
const segment = useSegment()

if (auth0.isAuthenticated && !auth0.isLoading) {
  segment.identify({ email: user.value.email })
}

</script>

<template>
  <App v-if="auth0.isAuthenticated && !auth0.isLoading" />
</template>

I'm probably misunderstanding what you're driving at, but the above feels more like a route guard to me.

useSegment is used in Segment, which is a component. So somewhere, higher up the hierarchy, you want to avoid rendering this component until isLoading is false.

So the structure should be something along the lines of this:

  • Root
    • App; uses useAuth0 to pull in isLoading (and isAuthenticated depending on the use case) and shows/hides Segment
      • Segment; uses useSegment, user will be accurate

Are you suggesting some sort of wrapper component that prevents all subsequent components from rendering until the user is available

So basically, that is what I mean yes.

If you need to access the user in a non-reactive way, you need to wait until isLoading is no-longer true. Once it is no longer true, you can retrieve isAuthenticated.value and user.value and it should be accurate (as in: isAuthenticated is false or true, and user could be null or the actual value). How you wait until isLoading is true is up to you, but as the composable is executed on render, you might want to avoid rendering it in the first place. Additionally, you can also use a watcher, but I agree that that is a bit less neat to use.

I'm probably misunderstanding what you're driving at, but the above feels more like a route guard to me.

I can see why, but that's because of the isAuthenticated. You have two approaches:

  • use only isLoading: this just means you are "guarding" certain parts of your application to only render when our SDK is done loading (meaning you are waiting untill we know the value of isAuthenticated and user, which can be false and null. Or true and an actual user).
  • use isLoading and isAuthenticated: this probably feels like route guarding, but on a not-router-level. You arent protecting routes, you are hiding components for users that arent logged in, which is perfectly valid and common to do.

Which of the two you want/need is up to you. It looks like you might have enough with just using isLoading tho, as you want to wait until the SDK knows the accurate value of user, allowing for it to be both null or an actual user, which you then guard against in useSegment yourself to not call identify when the user is null.