cream
cream•2mo ago

Why does Convex spam function calls?

It's entirely possible I'm using it wrong, but it's spamming my calls without a care in the world.
const roundResult = useQuery(api.game.getLastRound, {})
const roundResult = useQuery(api.game.getLastRound, {})
It does this every half a second without seemingly any reason to do it, the data has not changed, nor am I interacting with the APP.
No description
76 Replies
Clever Tagline
Clever Tagline•2mo ago
Chances are you've got state updating somewhere that's causing those to reload more often than they need to. Can you share more of the app code relevant to where this is happening?
cream
creamOP•2mo ago
Let's start with auth, because this is using custom JWT
"use client"

import { useIdentityToken, usePrivy } from "@privy-io/react-auth"
import {
ConvexProviderWithAuth,
ConvexReactClient,
useMutation,
} from "convex/react"
import { ReactNode, useCallback, useEffect, useRef } from "react"
import { api } from "@/convex/_generated/api"

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)

function usePrivyAsConvexAuth() {
const { ready, authenticated } = usePrivy()
const token = useIdentityToken()
const fetchAccessToken = useCallback(
async ({ forceRefreshToken: _ }: { forceRefreshToken: boolean }) => {
return token.identityToken
},
[token]
)
return {
isLoading: !ready,
isAuthenticated: authenticated,
fetchAccessToken,
}
}

function EnsureUserOnAuth() {
const { ready, authenticated } = usePrivy()
const ensureUser = useMutation(api.users.ensureUser)
const ran = useRef(false)

useEffect(() => {
if (!ready || !authenticated) return
if (ran.current) return
ran.current = true

ensureUser().catch(err => {
console.error("ensureUser failed", err)
// optional: allow retry on future mounts
// ran.current = false;
})
}, [ready, authenticated, ensureUser])

return null
}

export function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ConvexProviderWithAuth client={convex} useAuth={usePrivyAsConvexAuth}>
<EnsureUserOnAuth />
{children}
</ConvexProviderWithAuth>
)
}
"use client"

import { useIdentityToken, usePrivy } from "@privy-io/react-auth"
import {
ConvexProviderWithAuth,
ConvexReactClient,
useMutation,
} from "convex/react"
import { ReactNode, useCallback, useEffect, useRef } from "react"
import { api } from "@/convex/_generated/api"

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)

function usePrivyAsConvexAuth() {
const { ready, authenticated } = usePrivy()
const token = useIdentityToken()
const fetchAccessToken = useCallback(
async ({ forceRefreshToken: _ }: { forceRefreshToken: boolean }) => {
return token.identityToken
},
[token]
)
return {
isLoading: !ready,
isAuthenticated: authenticated,
fetchAccessToken,
}
}

function EnsureUserOnAuth() {
const { ready, authenticated } = usePrivy()
const ensureUser = useMutation(api.users.ensureUser)
const ran = useRef(false)

useEffect(() => {
if (!ready || !authenticated) return
if (ran.current) return
ran.current = true

ensureUser().catch(err => {
console.error("ensureUser failed", err)
// optional: allow retry on future mounts
// ran.current = false;
})
}, [ready, authenticated, ensureUser])

return null
}

export function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ConvexProviderWithAuth client={convex} useAuth={usePrivyAsConvexAuth}>
<EnsureUserOnAuth />
{children}
</ConvexProviderWithAuth>
)
}
Second, relating to the first snippet I sent, this is the only way I'm using it
if (
!ready ||
isLoadingGameState ||
roundResult == undefined ||
(authenticated && isPendingCanBounce) ||
isFetchingCanBounce
) {
return (
<div className="flex h-full min-w-[400px] flex-col items-center justify-center gap-8 p-14 pb-22">
<Image
src="/img/deadcat_ghost.avif"
alt="loader"
width={100}
height={100}
className="animate-spin opacity-10 invert"
/>
</div>
)
}
if (
!ready ||
isLoadingGameState ||
roundResult == undefined ||
(authenticated && isPendingCanBounce) ||
isFetchingCanBounce
) {
return (
<div className="flex h-full min-w-[400px] flex-col items-center justify-center gap-8 p-14 pb-22">
<Image
src="/img/deadcat_ghost.avif"
alt="loader"
width={100}
height={100}
className="animate-spin opacity-10 invert"
/>
</div>
)
}
or
if (
ready &&
!isLoadingGameState &&
!roundResult != undefined &&
gameState &&
(gameState.ended || gameState.timeRemaining === 0)
) {
return (
<div className="flex h-full flex-col justify-between overflow-y-auto p-4 pb-1! lg:p-6 xl:p-8 2xl:p-10">
<div
className={cn(
"flex flex-col items-center",
isDesktop ? "gap-6" : "gap-4"
)}
>
<h2
className={cn(
"text-center font-accent break-words text-foreground",
isDesktop ? "text-4xl" : "text-2xl"
)}
>
Hypurrliquidation{" "}
{roundResult?.round ? `#${roundResult.round}` : ""} complete!
</h2>
<Image
src="/img/deadcat_soul.avif"
alt="Dead Cat"
width={360 * (isDesktop ? 0.8 : 0.6)}
height={420 * (isDesktop ? 0.8 : 0.6)}
className="select-none"
/>

<p className="text-center text-lg break-words whitespace-nowrap text-tertiary">
The kitty is dead. Revive it to play again.
</p>

<Button
variant="accentb"
className="w-full font-accent! text-2xl"
onClick={onRestart}
loading={loading}
>
Revive!
</Button>

<div className="flex w-full flex-col gap-2">
<TextDisplay
title={"Winner"}
text={
roundResult?.winner.startsWith("0x")
? shortenAddress(roundResult?.winner)
: roundResult?.winner || "Nobody"
}
loading={!roundResult}
className={isDesktop ? "py-2" : "py-1"}
/>
<TextDisplay
title={"Prize"}
text={`${parseHYPE(roundResult?.prizePool || 0n, 6)} HYPE`}
loading={!roundResult}
className={isDesktop ? "py-2" : "py-1"}
/>
<TextDisplay
title={"Revival Fee"}
text={`${parseHYPE(entropyFee || 0n, 6)} HYPE`}
loading={!roundResult}
className={isDesktop ? "py-2" : "py-1"}
/>
</div>
</div>
</div>
)
}
if (
ready &&
!isLoadingGameState &&
!roundResult != undefined &&
gameState &&
(gameState.ended || gameState.timeRemaining === 0)
) {
return (
<div className="flex h-full flex-col justify-between overflow-y-auto p-4 pb-1! lg:p-6 xl:p-8 2xl:p-10">
<div
className={cn(
"flex flex-col items-center",
isDesktop ? "gap-6" : "gap-4"
)}
>
<h2
className={cn(
"text-center font-accent break-words text-foreground",
isDesktop ? "text-4xl" : "text-2xl"
)}
>
Hypurrliquidation{" "}
{roundResult?.round ? `#${roundResult.round}` : ""} complete!
</h2>
<Image
src="/img/deadcat_soul.avif"
alt="Dead Cat"
width={360 * (isDesktop ? 0.8 : 0.6)}
height={420 * (isDesktop ? 0.8 : 0.6)}
className="select-none"
/>

<p className="text-center text-lg break-words whitespace-nowrap text-tertiary">
The kitty is dead. Revive it to play again.
</p>

<Button
variant="accentb"
className="w-full font-accent! text-2xl"
onClick={onRestart}
loading={loading}
>
Revive!
</Button>

<div className="flex w-full flex-col gap-2">
<TextDisplay
title={"Winner"}
text={
roundResult?.winner.startsWith("0x")
? shortenAddress(roundResult?.winner)
: roundResult?.winner || "Nobody"
}
loading={!roundResult}
className={isDesktop ? "py-2" : "py-1"}
/>
<TextDisplay
title={"Prize"}
text={`${parseHYPE(roundResult?.prizePool || 0n, 6)} HYPE`}
loading={!roundResult}
className={isDesktop ? "py-2" : "py-1"}
/>
<TextDisplay
title={"Revival Fee"}
text={`${parseHYPE(entropyFee || 0n, 6)} HYPE`}
loading={!roundResult}
className={isDesktop ? "py-2" : "py-1"}
/>
</div>
</div>
</div>
)
}
I can't see how this would trigger an infinite loop
cream
creamOP•2mo ago
Here is a log visualization
cream
creamOP•2mo ago
and this is sync ws
Sara
Sara•2mo ago
May I ask, where are you calling the functions from? the TextDisplay component? or is the compoonent wrapped outside of it?
cream
creamOP•2mo ago
Calling it from the main default exported component This component is always visible funny thing is it's doing this for every component that is currently visible and has a useQuery
Sara
Sara•2mo ago
I recommend looking into passing "skip" in the variables, so instead of {} use something like: hasToLoad ? undefined:"skip" into the useQuery, but, I'm guessing that you need to look into the top level compoennt where you're calling this functions from, on each render, there will be a function call You shouldn't pass empty objects into the query call if no object is expected pass undefined
cream
creamOP•2mo ago
Doing useQuery(api.game.getLastRound) or useQuery(api.game.getLastRound, undefined) doesn't have any effect doing useQuery(api.game.getLastRound, "skip") works, but it defeats the purpose of use query
Sara
Sara•2mo ago
but to be fair, the caching works perfectly haha
cream
creamOP•2mo ago
caching works definetely im just worried about the function call number
Sara
Sara•2mo ago
no no, this is not what I recommended this is simply skipping the function call entirely
cream
creamOP•2mo ago
usefull in case I want to enable it after some condition what other context can I provide? I'm using Next 15.5.2, don't know if that matters or not mutations seem to be fine, queries are spamming every 300 millisesconds or so or is that regular behaviour?
Sara
Sara•2mo ago
deen
deen•2mo ago
does it still happen if you comment out <EnsureUserOnAuth />
Sara
Sara•2mo ago
that's also a good point
deen
deen•2mo ago
i don't know what that's doing, but... i sense something...
cream
creamOP•2mo ago
yep it's still going at it even with that commented out i'm sensing it's the auth as well, but I just can't understand what
// convex/auth.config.ts
export default {
providers: [
{
type: "customJwt",
applicationID: process.env.PRIVY_APP_ID,
issuer: "privy.io",
jwks: process.env.PRIVY_JWKS_URL,
algorithm: "ES256",
},
],
} as const;
// convex/auth.config.ts
export default {
providers: [
{
type: "customJwt",
applicationID: process.env.PRIVY_APP_ID,
issuer: "privy.io",
jwks: process.env.PRIVY_JWKS_URL,
algorithm: "ES256",
},
],
} as const;
Sara
Sara•2mo ago
cream, do me a favour, can you share the code of where the component where the query gets called? like parent of the child node
cream
creamOP•2mo ago
so page.tsx (parent) -> BounceControl.tsx (child w/ the useQuery) you need the page.tsx?
Sara
Sara•2mo ago
the bouncecontrol is where it gets called? can you share the part of where that component gets called?
cream
creamOP•2mo ago
Discord doesn't let me send the whole thing, but here is the return of page.tsx
return (
<>
{!mounted && (
<div className="flex h-full w-full flex-1 items-center justify-center">
<Loader2 size={64} className="animate-spin" />
</div>
)}

{/* Desktop View */}
{mounted && isDesktop && (
<div className="relative flex flex-1 overflow-x-hidden pb-4">
<div className="flex flex-1 flex-col overflow-hidden">
<div className="box-content h-[120px] px-4 py-2">
<StatsRow />
</div>
<PanelGroup direction="vertical">
<Panel className="flex-1 overflow-auto px-4" defaultSize={60}>
{isLoading && <Loader2 size={64} className="animate-spin" />}
{!isLoading && gameState != undefined && (
<Chart gameState={gameState} />
)}
</Panel>
<PanelResizeHandle className="group mx-4 flex h-5 w-[calc(100%-2rem)] items-center justify-center rounded-lg bg-border/30 transition-colors hover:bg-border/50">
<div className="flex items-center justify-center space-x-1 rounded-full bg-neutral-active px-2 py-0.5 transition-all duration-200 group-hover:bg-neutral">
<div className="h-1 w-1 rounded-full bg-accent transition-colors duration-200 group-hover:bg-accent/80"></div>
<div className="h-1 w-1 rounded-full bg-accent transition-colors duration-200 group-hover:bg-accent/80"></div>
<div className="h-1 w-1 rounded-full bg-accent transition-colors duration-200 group-hover:bg-accent/80"></div>
</div>
</PanelResizeHandle>
<Panel
className="flex max-h-[450px] flex-1 flex-col"
defaultSize={40}
>
<RoundStats />
</Panel>
</PanelGroup>
</div>

<div className="max-w-[400px] rounded-bl-lg border-b border-l border-border">
<BounceControl />
</div>
</div>
)}

{/* Mobile View */}
{mounted && !isDesktop && (
<>
<div className="flex flex-1 flex-col overflow-hidden">
<div className="h-[90px] border-b border-border px-1">
<StatsRow />
</div>
{activeTab === "chart" && (
<>
<div className="flex-1 overflow-auto">
{isLoading && <Loader2 size={64} className="animate-spin" />}
{!isLoading && gameState != undefined && (
<Chart gameState={gameState} />
)}
</div>
</>
)}
{activeTab === "bounce" && (
<div className="flex-1 overflow-auto px-6 py-0">
<BounceControl />
</div>
)}

{activeTab === "stats" && (
<div className="- overflow-aut flex-1 py-0">
<RoundStats className="mx-0 border-none" />
</div>
)}
</div>
<MobileMenu
activeTab={activeTab}
setActiveTab={setActiveTab}
openChat={openChat}
/>
</>
)}
</>
)
return (
<>
{!mounted && (
<div className="flex h-full w-full flex-1 items-center justify-center">
<Loader2 size={64} className="animate-spin" />
</div>
)}

{/* Desktop View */}
{mounted && isDesktop && (
<div className="relative flex flex-1 overflow-x-hidden pb-4">
<div className="flex flex-1 flex-col overflow-hidden">
<div className="box-content h-[120px] px-4 py-2">
<StatsRow />
</div>
<PanelGroup direction="vertical">
<Panel className="flex-1 overflow-auto px-4" defaultSize={60}>
{isLoading && <Loader2 size={64} className="animate-spin" />}
{!isLoading && gameState != undefined && (
<Chart gameState={gameState} />
)}
</Panel>
<PanelResizeHandle className="group mx-4 flex h-5 w-[calc(100%-2rem)] items-center justify-center rounded-lg bg-border/30 transition-colors hover:bg-border/50">
<div className="flex items-center justify-center space-x-1 rounded-full bg-neutral-active px-2 py-0.5 transition-all duration-200 group-hover:bg-neutral">
<div className="h-1 w-1 rounded-full bg-accent transition-colors duration-200 group-hover:bg-accent/80"></div>
<div className="h-1 w-1 rounded-full bg-accent transition-colors duration-200 group-hover:bg-accent/80"></div>
<div className="h-1 w-1 rounded-full bg-accent transition-colors duration-200 group-hover:bg-accent/80"></div>
</div>
</PanelResizeHandle>
<Panel
className="flex max-h-[450px] flex-1 flex-col"
defaultSize={40}
>
<RoundStats />
</Panel>
</PanelGroup>
</div>

<div className="max-w-[400px] rounded-bl-lg border-b border-l border-border">
<BounceControl />
</div>
</div>
)}

{/* Mobile View */}
{mounted && !isDesktop && (
<>
<div className="flex flex-1 flex-col overflow-hidden">
<div className="h-[90px] border-b border-border px-1">
<StatsRow />
</div>
{activeTab === "chart" && (
<>
<div className="flex-1 overflow-auto">
{isLoading && <Loader2 size={64} className="animate-spin" />}
{!isLoading && gameState != undefined && (
<Chart gameState={gameState} />
)}
</div>
</>
)}
{activeTab === "bounce" && (
<div className="flex-1 overflow-auto px-6 py-0">
<BounceControl />
</div>
)}

{activeTab === "stats" && (
<div className="- overflow-aut flex-1 py-0">
<RoundStats className="mx-0 border-none" />
</div>
)}
</div>
<MobileMenu
activeTab={activeTab}
setActiveTab={setActiveTab}
openChat={openChat}
/>
</>
)}
</>
)
that holds BounceControl
Sara
Sara•2mo ago
where do you change active tab?
cream
creamOP•2mo ago
and this is the top of BounceControl
export default function BounceControl() {
const { ready, authenticated } = usePrivy()
const [loading, setLoading] = useState(false)
const client = useQueryClient()
const { login } = useLogin()
const { isLoading: isLoadingGameState, data: gameState } = useGameState()
const { playMeow } = useKittySFX()

const roundResult = useQuery(api.game.getLastRound)
export default function BounceControl() {
const { ready, authenticated } = usePrivy()
const [loading, setLoading] = useState(false)
const client = useQueryClient()
const { login } = useLogin()
const { isLoading: isLoadingGameState, data: gameState } = useGameState()
const { playMeow } = useKittySFX()

const roundResult = useQuery(api.game.getLastRound)
Sara
Sara•2mo ago
where does the variable activeTab get changed?
cream
creamOP•2mo ago
in here
import React from "react"
import { cn } from "@/lib/utils"

type Tabs = "chart" | "bounce" | "chat" | "stats"

export const tabs: Tabs[] = ["chart", "bounce", "stats", "chat"]

export default function MobileMenu({
activeTab,
setActiveTab,
openChat,
}: {
activeTab: string
setActiveTab: (tab: Tabs) => void
openChat: () => void
}) {
return (
<div className="flex h-16 w-full border-t border-border bg-background">
{tabs.map((tab, i) => {
const isLastTab = tabs.length - 1 === i
return (
<React.Fragment key={tab}>
<div
className={cn(
"box-border flex h-full w-full flex-1 items-center justify-center",
activeTab === tab && "bg-neutral"
)}
onClick={() => {
if (tab === "chat") {
openChat()
return
}
setActiveTab(tab)
}}
>
<p className="font-accent text-2xl capitalize">{tab}</p>
</div>
{!isLastTab && <span className="h-full w-0.5 bg-border"></span>}
</React.Fragment>
)
})}
</div>
)
}
import React from "react"
import { cn } from "@/lib/utils"

type Tabs = "chart" | "bounce" | "chat" | "stats"

export const tabs: Tabs[] = ["chart", "bounce", "stats", "chat"]

export default function MobileMenu({
activeTab,
setActiveTab,
openChat,
}: {
activeTab: string
setActiveTab: (tab: Tabs) => void
openChat: () => void
}) {
return (
<div className="flex h-16 w-full border-t border-border bg-background">
{tabs.map((tab, i) => {
const isLastTab = tabs.length - 1 === i
return (
<React.Fragment key={tab}>
<div
className={cn(
"box-border flex h-full w-full flex-1 items-center justify-center",
activeTab === tab && "bg-neutral"
)}
onClick={() => {
if (tab === "chat") {
openChat()
return
}
setActiveTab(tab)
}}
>
<p className="font-accent text-2xl capitalize">{tab}</p>
</div>
{!isLastTab && <span className="h-full w-0.5 bg-border"></span>}
</React.Fragment>
)
})}
</div>
)
}
however, this is unseen for my desktop version
deen
deen•2mo ago
there's a lot going on here. what can you comment out to make it stop happening?
Sara
Sara•2mo ago
give me a min to read
cream
creamOP•2mo ago
removed everything
"use client"

import StatsRow from "@/components/StatsRow"
import BounceControl from "@/components/BounceControl"
import { useMediaQuery } from "usehooks-ts"
import { useEffect, useState } from "react"
import { Loader2 } from "lucide-react"
import MobileMenu from "@/components/MobileMenu"
import { minWidthDesktop } from "@/config/ui"
import { useChatContext } from "@/components/AppLayout"
import RoundStats from "@/components/RoundStats/RoundStats"
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"
import Chart from "@/components/Chart"
import { useGameState } from "@/hooks/useGameState"

/**
* Home page component.
*/
export default function Home() {
const isDesktop = useMediaQuery(`(min-width: ${minWidthDesktop}px)`)
const [mounted, setMounted] = useState(false)
const [activeTab, setActiveTab] = useState("chart")
const { openChat } = useChatContext()

const { data: gameState, isLoading } = useGameState()

useEffect(() => {
setMounted(true)
}, [])

return (
<>
<BounceControl />
</>
)
}
"use client"

import StatsRow from "@/components/StatsRow"
import BounceControl from "@/components/BounceControl"
import { useMediaQuery } from "usehooks-ts"
import { useEffect, useState } from "react"
import { Loader2 } from "lucide-react"
import MobileMenu from "@/components/MobileMenu"
import { minWidthDesktop } from "@/config/ui"
import { useChatContext } from "@/components/AppLayout"
import RoundStats from "@/components/RoundStats/RoundStats"
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"
import Chart from "@/components/Chart"
import { useGameState } from "@/hooks/useGameState"

/**
* Home page component.
*/
export default function Home() {
const isDesktop = useMediaQuery(`(min-width: ${minWidthDesktop}px)`)
const [mounted, setMounted] = useState(false)
const [activeTab, setActiveTab] = useState("chart")
const { openChat } = useChatContext()

const { data: gameState, isLoading } = useGameState()

useEffect(() => {
setMounted(true)
}, [])

return (
<>
<BounceControl />
</>
)
}
still hammering
Sara
Sara•2mo ago
could you console log the activeTab variable, in the component MobileMenu, and in the Page with a useEffect? if it gets changed so many times, that's where you need to look on every change, the component re-renders, and so do the function calls
cream
creamOP•2mo ago
I see, is there any extension I can use to see these react rerenders?
Sara
Sara•2mo ago
uhhh you can use the default dev tools the rendering tab, but with a console log and a useEffect it does the same thing HAHAHA, give me a moment to take a better screenshot
Sara
Sara•2mo ago
No description
cream
creamOP•2mo ago
rn im down to just this, and it still hammers away
export default function Home() {
return (
<>
<BounceControl />
</>
)
}
export default function Home() {
return (
<>
<BounceControl />
</>
)
}
which is the rendering tab?
deen
deen•2mo ago
this is showing auth looping, right? that would constantly invalidate your queries
cream
creamOP•2mo ago
exactly but why is auth looping because i've commented out everything else, and it still hammers so it may be the auth here are my two files related to auth convexprovider.tsx
"use client"

import { useIdentityToken, usePrivy } from "@privy-io/react-auth"
import {
ConvexProviderWithAuth,
ConvexReactClient,
useMutation,
} from "convex/react"
import { ReactNode, useCallback, useEffect, useRef } from "react"
import { api } from "@/convex/_generated/api"

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)

function usePrivyAsConvexAuth() {
const { ready, authenticated } = usePrivy()
const token = useIdentityToken()
const fetchAccessToken = useCallback(
async ({ forceRefreshToken: _ }: { forceRefreshToken: boolean }) => {
return token.identityToken
},
[token]
)
return {
isLoading: !ready,
isAuthenticated: authenticated,
fetchAccessToken,
}
}

function EnsureUserOnAuth() {
const { ready, authenticated } = usePrivy()
const ensureUser = useMutation(api.users.ensureUser)
const ran = useRef(false)

useEffect(() => {
if (!ready || !authenticated) return
if (ran.current) return
ran.current = true

ensureUser().catch(err => {
console.error("ensureUser failed", err)
// optional: allow retry on future mounts
// ran.current = false;
})
}, [ready, authenticated, ensureUser])

return null
}

export function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ConvexProviderWithAuth client={convex} useAuth={usePrivyAsConvexAuth}>
{/*<EnsureUserOnAuth />*/}
{children}
</ConvexProviderWithAuth>
)
}
"use client"

import { useIdentityToken, usePrivy } from "@privy-io/react-auth"
import {
ConvexProviderWithAuth,
ConvexReactClient,
useMutation,
} from "convex/react"
import { ReactNode, useCallback, useEffect, useRef } from "react"
import { api } from "@/convex/_generated/api"

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)

function usePrivyAsConvexAuth() {
const { ready, authenticated } = usePrivy()
const token = useIdentityToken()
const fetchAccessToken = useCallback(
async ({ forceRefreshToken: _ }: { forceRefreshToken: boolean }) => {
return token.identityToken
},
[token]
)
return {
isLoading: !ready,
isAuthenticated: authenticated,
fetchAccessToken,
}
}

function EnsureUserOnAuth() {
const { ready, authenticated } = usePrivy()
const ensureUser = useMutation(api.users.ensureUser)
const ran = useRef(false)

useEffect(() => {
if (!ready || !authenticated) return
if (ran.current) return
ran.current = true

ensureUser().catch(err => {
console.error("ensureUser failed", err)
// optional: allow retry on future mounts
// ran.current = false;
})
}, [ready, authenticated, ensureUser])

return null
}

export function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ConvexProviderWithAuth client={convex} useAuth={usePrivyAsConvexAuth}>
{/*<EnsureUserOnAuth />*/}
{children}
</ConvexProviderWithAuth>
)
}
and inside convex folder
// convex/auth.config.ts
export default {
providers: [
{
type: "customJwt",
applicationID: process.env.PRIVY_APP_ID,
issuer: "privy.io",
jwks: process.env.PRIVY_JWKS_URL,
algorithm: "ES256",
},
],
} as const;
// convex/auth.config.ts
export default {
providers: [
{
type: "customJwt",
applicationID: process.env.PRIVY_APP_ID,
issuer: "privy.io",
jwks: process.env.PRIVY_JWKS_URL,
algorithm: "ES256",
},
],
} as const;
deen
deen•2mo ago
i dunno i just use clerk 🤪
cream
creamOP•2mo ago
not an option sadly
Sara
Sara•2mo ago
I've lost track of everything. sorry I won't be able to help much in here 😅
cream
creamOP•2mo ago
Ok, here to help with more info It's definetely the Auth as I've removed it completely and now it finally works as it should but I need my auth wow, it's refreshing to see it behave normally
Sara
Sara•2mo ago
Yeah I don't have much information, but can try to run the code later today, I'd look into the useEffect
cream
creamOP•2mo ago
that is commented out
Sara
Sara•2mo ago
comment one thing after the other until you figure what's invalidiating it do you still see the logs if you remove it?
deen
deen•2mo ago
id never heard of Privy, and ive never dug into auth. but your problem is there
Sara
Sara•2mo ago
me too!
deen
deen•2mo ago
ive never even used convex auth haha...
Sara
Sara•2mo ago
you might want to check the custom convex-auth thingy for this, https://docs.convex.dev/auth/advanced/custom-auth "update the provider code to make a custom provider for your case"
cream
creamOP•2mo ago
oh yes it's spamming like crazy so it's not the use effect
Sara
Sara•2mo ago
cause I can see its memoised in the docs but not in your case
cream
creamOP•2mo ago
but rather this snippet
function usePrivyAsConvexAuth() {
const { ready, authenticated } = usePrivy()
const token = useIdentityToken()
const fetchAccessToken = useCallback(
async ({ forceRefreshToken: _ }: { forceRefreshToken: boolean }) => {
return token.identityToken
},
[token]
)
return {
isLoading: !ready,
isAuthenticated: authenticated,
fetchAccessToken,
}
}

export function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ConvexProviderWithAuth client={convex} useAuth={usePrivyAsConvexAuth}>
{children}
</ConvexProviderWithAuth>
)
function usePrivyAsConvexAuth() {
const { ready, authenticated } = usePrivy()
const token = useIdentityToken()
const fetchAccessToken = useCallback(
async ({ forceRefreshToken: _ }: { forceRefreshToken: boolean }) => {
return token.identityToken
},
[token]
)
return {
isLoading: !ready,
isAuthenticated: authenticated,
fetchAccessToken,
}
}

export function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ConvexProviderWithAuth client={convex} useAuth={usePrivyAsConvexAuth}>
{children}
</ConvexProviderWithAuth>
)
Sara
Sara•2mo ago
try memoising the return statement to the top
cream
creamOP•2mo ago
did that stil nothing changed
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)

function usePrivyAsConvexAuth() {
const { ready, authenticated } = usePrivy()
const token = useIdentityToken()

const fetchAccessToken = useCallback(
async ({ forceRefreshToken: _ }: { forceRefreshToken: boolean }) => {
return token.identityToken
},
[token]
)

return useMemo(
() => ({
isLoading: !ready,
isAuthenticated: authenticated,
fetchAccessToken,
}),
[ready, authenticated, fetchAccessToken]
)
}

export function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ConvexProviderWithAuth client={convex} useAuth={usePrivyAsConvexAuth}>
{children}
</ConvexProviderWithAuth>
)
}
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)

function usePrivyAsConvexAuth() {
const { ready, authenticated } = usePrivy()
const token = useIdentityToken()

const fetchAccessToken = useCallback(
async ({ forceRefreshToken: _ }: { forceRefreshToken: boolean }) => {
return token.identityToken
},
[token]
)

return useMemo(
() => ({
isLoading: !ready,
isAuthenticated: authenticated,
fetchAccessToken,
}),
[ready, authenticated, fetchAccessToken]
)
}

export function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ConvexProviderWithAuth client={convex} useAuth={usePrivyAsConvexAuth}>
{children}
</ConvexProviderWithAuth>
)
}
Sara
Sara•2mo ago
I'll need to run it to debug
cream
creamOP•2mo ago
no worries I've managed to fix it needs to look like this
const { ready, authenticated } = usePrivy()
const { identityToken } = useIdentityToken()

// keep latest token in a ref so fetchAccessToken stays stable
const tokenRef = useRef<string | null>(null)
useEffect(() => {
tokenRef.current = identityToken ?? null
}, [identityToken])

const fetchAccessToken = useCallback(
async ({ forceRefreshToken: _ }: { forceRefreshToken: boolean }) => {
// If Privy isn't ready or user isn't authed, tell Convex "no token".
if (!ready || !authenticated) return null

// We can't force refresh on Privy; just return whatever we have now.
// Convex accepts null (no token yet) or a string token.
const current = tokenRef.current
return current ?? null
},
[ready, authenticated]
)

return useMemo(
() => ({
// Don't flip out of loading until we actually *have* a token when authed.
isLoading: !ready || (authenticated && !identityToken),
isAuthenticated: authenticated,
fetchAccessToken,
}),
[ready, authenticated, identityToken, fetchAccessToken]
)
}
const { ready, authenticated } = usePrivy()
const { identityToken } = useIdentityToken()

// keep latest token in a ref so fetchAccessToken stays stable
const tokenRef = useRef<string | null>(null)
useEffect(() => {
tokenRef.current = identityToken ?? null
}, [identityToken])

const fetchAccessToken = useCallback(
async ({ forceRefreshToken: _ }: { forceRefreshToken: boolean }) => {
// If Privy isn't ready or user isn't authed, tell Convex "no token".
if (!ready || !authenticated) return null

// We can't force refresh on Privy; just return whatever we have now.
// Convex accepts null (no token yet) or a string token.
const current = tokenRef.current
return current ?? null
},
[ready, authenticated]
)

return useMemo(
() => ({
// Don't flip out of loading until we actually *have* a token when authed.
isLoading: !ready || (authenticated && !identityToken),
isAuthenticated: authenticated,
fetchAccessToken,
}),
[ready, authenticated, identityToken, fetchAccessToken]
)
}
Sara
Sara•2mo ago
Sweet!
jas0nw01
jas0nw01•2mo ago
hey @cream , mind sharing how are you handling the signIn for users in your privy useLogin's onComplete callback? im currently using the signIn from
import { useAuthActions } from "@convex-dev/auth/react"

...

const { signIn, signOut } = useAuthActions()
import { useAuthActions } from "@convex-dev/auth/react"

...

const { signIn, signOut } = useAuthActions()
cream
creamOP•2mo ago
what you see above is what im using and it works for me
jas0nw01
jas0nw01•2mo ago
thanks, i took a look but i dont see any login above? 😅
cream
creamOP•2mo ago
ah, i see Privy exposes login hooks I’ll send them here tommorow
jas0nw01
jas0nw01•2mo ago
thank you! yeah im doing that, i just wonder how to connect that with my convex database. my code looks like this now
const { login: privyLogin } = useLogin({
onComplete: async ({ user }) => {
try {
const token = await getPrivyToken()
if (!token) {
throw new Error("No token found")
}

// this part is not working nicely
await signIn("credentials", {
token,
email,
account,
privyId: user.id
})
} catch (error) {
console.error("Failed to create user:", error)
}
},
onError: (error: PrivyErrorCode) => {
if (error !== "exited_auth_flow") {
console.error("Login failed:", error)
}
}
})
const { login: privyLogin } = useLogin({
onComplete: async ({ user }) => {
try {
const token = await getPrivyToken()
if (!token) {
throw new Error("No token found")
}

// this part is not working nicely
await signIn("credentials", {
token,
email,
account,
privyId: user.id
})
} catch (error) {
console.error("Failed to create user:", error)
}
},
onError: (error: PrivyErrorCode) => {
if (error !== "exited_auth_flow") {
console.error("Login failed:", error)
}
}
})
cream
creamOP•2mo ago
You are overcomplicating it Privy does the auth on the client And on convex you just setup the proper config to read jwts from privy See above code I shared
jas0nw01
jas0nw01•2mo ago
yea i have all the config and jwts too. but how do u register a new user in convex database? basically that's what my signIn does 😅
cream
creamOP•2mo ago
Ah I see You want to store the user in the db as well
jas0nw01
jas0nw01•2mo ago
yea, do you have frontend code that does that?
cream
creamOP•2mo ago
Yes I have a react component that calls a mutation
jas0nw01
jas0nw01•2mo ago
my signIn is a convex backend func that basically does
const claims = await privy.verifyAuthToken(args.token)
if (claims.userId !== args.privyId) {
throw new Error("Privy user ID mismatch")
}

const userId = await ctx.db.insert("users", {
walletAddress: args.walletAddress?.toLowerCase() || "",
email: args.email,
privyId: args.privyId
})
const claims = await privy.verifyAuthToken(args.token)
if (claims.userId !== args.privyId) {
throw new Error("Privy user ID mismatch")
}

const userId = await ctx.db.insert("users", {
walletAddress: args.walletAddress?.toLowerCase() || "",
email: args.email,
privyId: args.privyId
})
oh u're not doing the privy.verifyAuthToken?
cream
creamOP•2mo ago
no
jas0nw01
jas0nw01•2mo ago
got it
cream
creamOP•2mo ago
if config is setup correctly, convex should be doing that automatically
jas0nw01
jas0nw01•2mo ago
interesting. then i can remove it as well
cream
creamOP•2mo ago
jwks url is the means of verification Hence you dont need to double verify
jas0nw01
jas0nw01•2mo ago
is your mutation setup in such a way that it'd always run when a user lands on the page? or only when it's a first time user
cream
creamOP•2mo ago
By the time it reaches your functions its already good Its one of the parent components And its idempotent Basically if the user exists i dont do anything / or u could update missing, or different fields
jas0nw01
jas0nw01•2mo ago
function EnsureUserOnAuth() {
const { ready, authenticated } = usePrivy()
const ensureUser = useMutation(api.users.ensureUser)
const ran = useRef(false)

useEffect(() => {
if (!ready || !authenticated) return
if (ran.current) return
ran.current = true

ensureUser().catch(err => {
console.error("ensureUser failed", err)
// optional: allow retry on future mounts
// ran.current = false;
})
}, [ready, authenticated, ensureUser])

return null
}
function EnsureUserOnAuth() {
const { ready, authenticated } = usePrivy()
const ensureUser = useMutation(api.users.ensureUser)
const ran = useRef(false)

useEffect(() => {
if (!ready || !authenticated) return
if (ran.current) return
ran.current = true

ensureUser().catch(err => {
console.error("ensureUser failed", err)
// optional: allow retry on future mounts
// ran.current = false;
})
}, [ready, authenticated, ensureUser])

return null
}
here right?
cream
creamOP•2mo ago
Its a hackish way, because u dont really have a privy cookbook recipe rn Yeah Thats what im using
jas0nw01
jas0nw01•2mo ago
ok thank you! i'll try that 🙂 haha im just relying on the onComplete callback from useLogin
dan myles
dan myles•3w ago
@cream do you have a complete example for this? having trouble wiuth privy and convex as well

Did you find this page helpful?