oscklm
oscklm12mo ago

Help migrating to use withUser() from convex-helpers and questions regarding the library

Hello, so i recently started migrating over to use some of the nice helpers etc. that convex-helpers brings. I started with trying to refactor the way i handle auth and getting my user object, and ran into some minor issues, that raised some questions when using withUser() and useQuery() in a react component. Here are some code for context, and easier understanding. Questions are listed below the code snippets
// withUser.ts
import { customCtx } from 'convex-helpers/server/customFunctions'
import { zCustomMutation, zCustomQuery } from 'convex-helpers/server/zod'

import { QueryCtx, mutation, query } from '../_generated/server'

export async function getUserByTokenIdentifier<Ctx extends QueryCtx>(ctx: Ctx) {
const identity = await ctx.auth.getUserIdentity()
if (!identity) {
throw new Error('Unauthenticated call to function requiring authentication')
}
const user = await ctx.db
.query('user')
.withIndex('by_token', (q) => q.eq('tokenIdentifier', identity.tokenIdentifier))
.unique()
if (!user) throw new Error('User not found')
return user
}

const addUser = customCtx(async (ctx: QueryCtx) => ({
user: await getUserByTokenIdentifier(ctx),
}))

export const mutationWithUser = zCustomMutation(mutation, addUser)

export const queryWithUser = zCustomQuery(query, addUser)
// withUser.ts
import { customCtx } from 'convex-helpers/server/customFunctions'
import { zCustomMutation, zCustomQuery } from 'convex-helpers/server/zod'

import { QueryCtx, mutation, query } from '../_generated/server'

export async function getUserByTokenIdentifier<Ctx extends QueryCtx>(ctx: Ctx) {
const identity = await ctx.auth.getUserIdentity()
if (!identity) {
throw new Error('Unauthenticated call to function requiring authentication')
}
const user = await ctx.db
.query('user')
.withIndex('by_token', (q) => q.eq('tokenIdentifier', identity.tokenIdentifier))
.unique()
if (!user) throw new Error('User not found')
return user
}

const addUser = customCtx(async (ctx: QueryCtx) => ({
user: await getUserByTokenIdentifier(ctx),
}))

export const mutationWithUser = zCustomMutation(mutation, addUser)

export const queryWithUser = zCustomQuery(query, addUser)
3 Replies
oscklm
oscklmOP12mo ago
How i utilize it in my AuthProvider:
// user/queries.ts
import { queryWithUser } from '@/convex/lib/withUser'

export const getCurrent = queryWithUser({
handler: async (ctx) => ctx.user,
})

// auth-provider.tsx
import { useConvexAuth, useMutation, useQuery } from 'convex/react'
import { usePostHog } from 'posthog-react-native'
import { createContext, useContext, useEffect } from 'react'

import LoadingView from '@/components/loading-view'
import { api } from '@/convex/_generated/api'
import { User } from '@/convex/user/schema'
import { UserProfile } from '@/convex/userProfile/schema'
import { useUser } from '@/lib/adapters/clerk'

type AuthContextType = {
user?: User | null
profile?: UserProfile
}

// Create a new context for the current profile from the user
const AuthContext = createContext<AuthContextType>({
user: undefined,
profile: undefined,
})

export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const { isAuthenticated, isLoading } = useConvexAuth()
const { user: clerkUser } = useUser()
const posthog = usePostHog()

// Convex mutations
const user = useQuery(api.user.queries.getCurrent, isAuthenticated ? {} : 'skip')
const storeUser = useMutation(api.user.mutations.store)

useEffect(() => {
if (!isAuthenticated) return

async function storeUserInConvex() {
await storeUser()
if (user) {
posthog?.identify(user._id, {
name: user.name,
})
}
}
storeUserInConvex()
}, [isAuthenticated, storeUser, clerkUser?.id])

if (isLoading) {
return <LoadingView />
}

return <AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
}

export const useAuth = () => useContext(AuthContext)
// user/queries.ts
import { queryWithUser } from '@/convex/lib/withUser'

export const getCurrent = queryWithUser({
handler: async (ctx) => ctx.user,
})

// auth-provider.tsx
import { useConvexAuth, useMutation, useQuery } from 'convex/react'
import { usePostHog } from 'posthog-react-native'
import { createContext, useContext, useEffect } from 'react'

import LoadingView from '@/components/loading-view'
import { api } from '@/convex/_generated/api'
import { User } from '@/convex/user/schema'
import { UserProfile } from '@/convex/userProfile/schema'
import { useUser } from '@/lib/adapters/clerk'

type AuthContextType = {
user?: User | null
profile?: UserProfile
}

// Create a new context for the current profile from the user
const AuthContext = createContext<AuthContextType>({
user: undefined,
profile: undefined,
})

export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const { isAuthenticated, isLoading } = useConvexAuth()
const { user: clerkUser } = useUser()
const posthog = usePostHog()

// Convex mutations
const user = useQuery(api.user.queries.getCurrent, isAuthenticated ? {} : 'skip')
const storeUser = useMutation(api.user.mutations.store)

useEffect(() => {
if (!isAuthenticated) return

async function storeUserInConvex() {
await storeUser()
if (user) {
posthog?.identify(user._id, {
name: user.name,
})
}
}
storeUserInConvex()
}, [isAuthenticated, storeUser, clerkUser?.id])

if (isLoading) {
return <LoadingView />
}

return <AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
}

export const useAuth = () => useContext(AuthContext)
Questions: 1. How might one properly handle the error thrown from getUserByTokenIdentifier(ctx) because i can't just wrap the useQuery() in a try catch in the react component, as far as im concerned. Couldn't find any examples on this. 2. As it can be seen, the way i get around the query throwing an error, is by using isAuthenticated in the args on the query in my auth-provider and skip it if its false. But wondering if there would be a smarter way of doing this, so i dont have to import useConvexAuth() everywhere i'd like to call a useQuery on query that utilizes withUser(), couldn't find any stack posts on how to build a custom useQuery() to go with a custom query like withUser()
lee
lee12mo ago
You usually want to use the <Authenticated> component to wrap any component that needs to call authenticated queries Like
import { Authenticated, Unauthenticated, AuthLoading } from "convex/react";

function AuthenticatedApp() {
const r = useQuery(api.myquery);
return <div>{r}</div>;
}

function App() {
return (
<div className="App">
<Authenticated><AuthenticatedApp /></Authenticated>
<Unauthenticated><MyLoginComponent/></Unauthenticated>
<AuthLoading>Still loading</AuthLoading>
</div>
);
}
import { Authenticated, Unauthenticated, AuthLoading } from "convex/react";

function AuthenticatedApp() {
const r = useQuery(api.myquery);
return <div>{r}</div>;
}

function App() {
return (
<div className="App">
<Authenticated><AuthenticatedApp /></Authenticated>
<Unauthenticated><MyLoginComponent/></Unauthenticated>
<AuthLoading>Still loading</AuthLoading>
</div>
);
}
oscklm
oscklmOP12mo ago
Ahh thanks Lee. This makes sense, i've missed the obvious here. Makes a lot of sense just wrapping my auth layout in my router, and all children routes that uses route. So the query would never run or happen.

Did you find this page helpful?