wobsoriano / vue-clerk

Unofficial Vue + Clerk integration.
https://vue-clerk.com
MIT License
108 stars 8 forks source link

Usage with Nuxt #40

Closed whalesalad closed 1 month ago

whalesalad commented 1 month ago

I am struggling to get the composables to work with Nuxt.

I have tried three approaches:

  1. Following the example from nuxt-clerk-template here: https://github.com/wobsoriano/nuxt-clerk-template/blob/main/plugins/clerk.client.ts
  2. Trying the nuxt-clerk module here: https://github.com/RodrigoProjects/nuxt-clerk (found via this issue: https://github.com/wobsoriano/vue-clerk/issues/12)
  3. Going the more vanilla route and following the example of how to integrate a vue plugin with nuxt in conjunction with the vue-clerk docs on using the plugin

I was able to render the auth component and actually login ... but trying to use any composables (ex useUser()) result in the same issue:

Here are the two methods attempted in plugins/clerk.client.ts:

Method A:

import { clerkPlugin } from 'vue-clerk/plugin'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(clerkPlugin, {
    publishableKey: import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
  })

  console.log({clerkPlugin, nuxtApp})
  console.log(`Clerk plugin loaded ${import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}`)
})

Method B:

import { Clerk, provideClerkToVueApp } from 'vue-clerk/plugin'

export default defineNuxtPlugin(async (nuxtApp) => {
  const isClerkLoaded = ref(false)

  const clerk = new Clerk(import.meta.env.VITE_CLERK_PUBLISHABLE_KEY)
  console.log(`Clerk plugin loaded ${import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}`)

  // Instead of using the `vue-clerk` plugin, we can use this internal function to create a Clerk instance.
  provideClerkToVueApp(nuxtApp.vueApp, clerk, {
    isClerkLoaded,
    shouldLoadClerk: false,
    clerkOptions: {}, // Optional since we run `clerk.load` manually
  })

  // This will make sure that the clerk library is loaded in the client first before moving on to the next middleware.
  await clerk.load({
    routerPush: (to) => {
      return navigateTo(to)
    },
    routerReplace: (to) => {
      return navigateTo(to, { replace: true })
    },
  })

  isClerkLoaded.value = true

  return {
    provide: {
      clerk
    },
  }
})

Any insights would be greatly appreciated, thank you.

whalesalad commented 1 month ago

Here is my package.json to give version numbers:

{
  "name": "atlas-nuxt",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare",
    "lint": "eslint .",
    "typecheck": "nuxt typecheck"
  },
  "dependencies": {
    "@iconify-json/heroicons": "^1.1.21",
    "@iconify-json/simple-icons": "^1.1.101",
    "@nuxt/content": "^2.12.1",
    "@nuxt/fonts": "^0.7.0",
    "@nuxt/image": "^1.7.0",
    "@nuxt/ui-pro": "^1.2.0",
    "@vueuse/nuxt": "^10.9.0",
    "nuxt": "^3.11.2",
    "nuxt-og-image": "^2.2.4",
    "vue-clerk": "^0.3.8"
  },
  "devDependencies": {
    "@nuxt/eslint": "^0.3.10",
    "@nuxthq/studio": "^1.0.15",
    "eslint": "^9.2.0",
    "nuxt-clerk": "^0.0.9",
    "vue-tsc": "^2.0.16"
  }
}

I do have the following in my nuxt.config.js as well:

build: {
  transpile: ['vue-clerk', '@clerk/clerk-js'],
},
wobsoriano commented 1 month ago

Hi @whalesalad! That error happens when you use the Clerk components and composables in a server rendered app. To fix this, you may want to wrap the Clerk component or component that uses a composable with a <ClientOnly> component from Nuxt.

See example here. Notice this part:

Screenshot 2024-06-06 at 2 29 36 PM

All of the components wrapped in there are using Clerk composables. If you remove the wrapper then you'll get the same error.

I'll definitely improve this more in the near future!

whalesalad commented 1 month ago

I tried this suggestion but unfortunately the problem remains:

Here is dashboard.vue:

<script setup lang="ts">
import { useUser } from 'vue-clerk'

const { user } = useUser()
</script>

<template>
  <div class="px-8 py-12 sm:py-16 md:px-20">
    <template v-if="user">
      <div class="grid gap-4 mt-8 lg:grid-cols-3">
        <ClientOnly>
          <h1 class="text-3xl font-semibold text-black">
            <pre>{{ user }}</pre>
          </h1>

          <!--
          <UserDetails />
          <SessionDetails />
          <OrgDetails />
          -->

          <template #fallback>
            <p>Loading Clerk components...</p>
          </template>

        </ClientOnly>
      </div>
    </template>
  </div>
</template>

and here is the result:

Screenshot_20240606_173525

Screenshot_20240606_173629

whalesalad commented 1 month ago

I removed the use of useUser() and things have changed at least but it does appear the plugin is not loading or being recognized by vue - all the components are not resolvable:

Screenshot_20240606_173839

whalesalad commented 1 month ago

Ok, some progress! Sorry to spam here but want to keep sharing in case it helps other people.

I dug into your remarks on "server rendered app" and am now forcing everything to be clientside by adding ssr: false to my nuxt config.

This is working, and I can use the useUser() now! Woohoo!

I see now that these compents are not included in vue-clerk but are part of the example, so that makes sense as to why they are not working.

Sorry I have a ton of experience with Vue, but this is my first project with Nuxt. I am not sure what implications this has for me in the future as far as a hybrid rendering, but things are unblocked now and working.

Thanks for your help!

wobsoriano commented 1 month ago

Hey, no problem! Let me explain more:

By default, SSR in Nuxt is enabled, which causes the Clerk components and composables to error out because they try to access the window object on the server. Setting SSR to false will resolve this issue, eliminating the need to wrap them with <ClientOnly>. I know the error messages are not related, but I'll improve them soon!

If you still want SSR and make the Clerk components/composables work, you will have to move your code here into another component, and wrap that with <ClientOnly> instead:

SomeComponent.vue

<script setup lang="ts">
import { useUser } from 'vue-clerk'

const { user } = useUser()
</script>

<template>
  <div class="px-8 py-12 sm:py-16 md:px-20">
    <template v-if="user">
      <div class="grid gap-4 mt-8 lg:grid-cols-3">
          <h1 class="text-3xl font-semibold text-black">
            <pre>{{ user }}</pre>
          </h1>
      </div>
    </template>
  </div>
</template>

Dashboard.vue

<template>
  <ClientOnly>
    <SomeComponent />
  </ClientOnly>
</template>

Why do we want to do this? Because even if you made this part of the code client only

Screenshot 2024-06-06 at 6 39 08 PM

You're still invoking the composable in the setup function and that will call the Clerk library and access the window object somewhere.