cream
cream•9h 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
54 Replies
Clever Tagline
Clever Tagline•9h 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•9h 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•9h ago
Here is a log visualization
cream
creamOP•9h ago
and this is sync ws
Sara
Sara•9h ago
May I ask, where are you calling the functions from? the TextDisplay component? or is the compoonent wrapped outside of it?
cream
creamOP•9h 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•9h 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•9h 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•9h ago
but to be fair, the caching works perfectly haha
cream
creamOP•9h ago
caching works definetely im just worried about the function call number
Sara
Sara•9h ago
no no, this is not what I recommended this is simply skipping the function call entirely
cream
creamOP•9h 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•9h ago
deen
deen•9h ago
does it still happen if you comment out <EnsureUserOnAuth />
Sara
Sara•9h ago
that's also a good point
deen
deen•9h ago
i don't know what that's doing, but... i sense something...
cream
creamOP•9h 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•9h 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•9h ago
so page.tsx (parent) -> BounceControl.tsx (child w/ the useQuery) you need the page.tsx?
Sara
Sara•9h ago
the bouncecontrol is where it gets called? can you share the part of where that component gets called?
cream
creamOP•9h 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•9h ago
where do you change active tab?
cream
creamOP•9h 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•9h ago
where does the variable activeTab get changed?
cream
creamOP•9h 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•9h ago
there's a lot going on here. what can you comment out to make it stop happening?
Sara
Sara•9h ago
give me a min to read
cream
creamOP•9h 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•9h 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•9h ago
I see, is there any extension I can use to see these react rerenders?
Sara
Sara•9h 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•9h ago
No description
cream
creamOP•8h 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•8h ago
this is showing auth looping, right? that would constantly invalidate your queries
cream
creamOP•8h 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•8h ago
i dunno i just use clerk 🤪
cream
creamOP•8h ago
not an option sadly
Sara
Sara•8h ago
I've lost track of everything. sorry I won't be able to help much in here 😅
cream
creamOP•8h 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•8h 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•8h ago
that is commented out
Sara
Sara•8h 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•8h ago
id never heard of Privy, and ive never dug into auth. but your problem is there
Sara
Sara•8h ago
me too!
deen
deen•8h ago
ive never even used convex auth haha...
Sara
Sara•8h 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•8h ago
oh yes it's spamming like crazy so it's not the use effect
Sara
Sara•8h ago
cause I can see its memoised in the docs but not in your case
cream
creamOP•8h 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•8h ago
try memoising the return statement to the top
cream
creamOP•8h 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•8h ago
I'll need to run it to debug
cream
creamOP•8h 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•8h ago
Sweet!

Did you find this page helpful?