#usePreloadedAuthQuery causes UI flash that usePreloadedQuery doesn't
29 messages · Page 1 of 1 (latest)
You’re running into a known trade‑off between “auth‑aware” hydration and “raw” hydration.
From the Better Auth docs:
usePreloadedQuerysimply hydrates from the server‑renderedPreloadedpayload and then switches to the live client query. [usePreloadedQuery]usePreloadedAuthQueryis explicitly designed to hold on to the server‑rendered data until authentication is ready, i.e. it coordinates Convex query hydration with Better Auth’s auth state. [usePreloadedAuthQuery]
In the Better Auth migration guide, the example shows:
const userQuery = usePreloadedAuthQuery(preloadedUserQuery);
with the description:
usePreloadedAuthQueryreplacesusePreloadedQueryas a drop-in – it ensures server rendered data is rendered until authentication is ready. [usePreloadedAuthQuery]
So if you’re seeing a brief undefined during hydration with usePreloadedAuthQuery but not with usePreloadedQuery, that’s likely due to how your auth state is being resolved on the client (e.g. a short “auth is loading” window) rather than a documented behavioral bug in Convex itself. The docs don’t provide a knob to change this behavior or an official workaround beyond using the auth‑aware hook.
The knowledge sources don’t describe a configuration or code change that would remove that flash specifically for usePreloadedAuthQuery; they only state its intended behavior and motivation. I’d recommend:
- Double‑checking that your Better Auth setup is passing server auth state into the client (to avoid an “auth loading” flash), similar to the SvelteKit SSR pattern where server auth state is injected to prevent loading flashes. [SSR optional]
- If the flash persists, opening an issue or asking in Discord with a minimal repro, since the API design around these auth‑aware preloaded hooks is still evolving and the docs explicitly call out UX around flashes as an area of active improvement. [How to use preload query with authentication; Better-auth Next.js usage]
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@reef vessel Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@jovial kelp would you remmoned instead using usepreloadedquery?
Let me check the knowledge base for you again. One moment! :books:
I’m sorry, but the knowledge sources don’t explicitly recommend choosing usePreloadedQuery over usePreloadedAuthQuery (or vice versa) for your flashing issue, so I can’t give a definitive “you should switch” answer.
What is documented:
usePreloadedAuthQueryis intended as a drop‑in replacement forusePreloadedQuerythat “ensures server rendered data is rendered until authentication is ready.” [usePreloadedAuthQuery]- It exists specifically to solve auth‑related race conditions between Convex query hydration and auth state, which
usePreloadedQueryby itself doesn’t handle. [Better Auth Next.js usage]
So, based on the docs:
- If your queries require authentication and you’re using Better Auth, the intended hook is
usePreloadedAuthQuery, even if you’re currently seeing a flash. - If the flash is unacceptable, the sources suggest improving SSR auth state (passing server auth state to the client to avoid an “auth loading” phase) rather than reverting to
usePreloadedQuery. [SSR optional]
Given what’s documented, the “recommended” path is to keep usePreloadedAuthQuery for authed data and report the flashing behavior (with a repro) rather than switching back to usePreloadedQuery just to avoid the flash.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@reef vessel Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@jovial kelp but im checking for auth state on the server right? doesnt that solve the issue of "ensures server rendered data is rendered until authentication is ready."
Let me check the knowledge base for you again. One moment! :books:
You’re right that doing auth on the server should largely solve the “auth not ready yet” problem, but the docs don’t spell out how usePreloadedAuthQuery behaves specifically when you already have server auth state wired through, so I can’t say with certainty that it will never produce a flash in that setup.
What the sources do say:
usePreloadedAuthQueryexists to “ensure server rendered data is rendered until authentication is ready.” [usePreloadedAuthQuery]- To actually avoid the “auth is loading” flash, Better Auth recommends passing server auth state into the client so the client doesn’t start in an “unknown” state. The SvelteKit guide shows this pattern: get auth state on the server, then pass it into the client auth client so
isLoadingis false andisAuthenticatedis immediately correct. [SSR optional]
So:
- Checking auth on the server alone is not enough; you also need to hydrate the client auth layer with that server state (so the client doesn’t briefly think “auth is loading / unknown”).
usePreloadedAuthQueryis designed to work in that world: it keeps using the preloaded data until the client auth state is ready. [usePreloadedAuthQuery]
The knowledge sources don’t show the exact Next.js wiring for “pass server auth state into the Better Auth client,” only the SvelteKit example, so I can’t give you a concrete Next.js snippet. But conceptually, you want the same pattern: server layout gets auth state → passes it into the client auth provider → usePreloadedAuthQuery no longer has a “gap” where auth is unknown.
If you’ve already done that wiring and still see undefined briefly, that’s beyond what the docs cover; in that case, sharing a minimal repro in Discord or GitHub would be the next step.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@reef vessel Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@jovial kelp but im using nextjs
Let me check the knowledge base for you again. One moment! :books:
You’re right to focus on Next.js; the SvelteKit example I mentioned is just the clearest pattern we have documented, but there isn’t an equivalent, fully‑spelled‑out Next.js auth‑state‑hydration example in the sources.
What is documented for Next.js + Better Auth:
- For SSR with server components, you should use
preloadAuthQueryon the server andusePreloadedAuthQueryin client components. [Next.js usage] usePreloadedAuthQueryis intended as a drop‑in replacement forusePreloadedQuerythat “ensures server rendered data is rendered until authentication is ready.” [usePreloadedAuthQuery]
Separately, the SvelteKit guide shows the general Better Auth pattern to avoid flashes:
- Get auth state on the server (
getAuthState(createAuth, cookies)). - Pass that state into the client auth client (
getServerState: () => data.authState), so the client doesn’t start in a “loading” state. [SSR optional]
The docs do not currently show the exact Next.js equivalent of that “pass server auth state into the client” wiring, so I can’t give you a concrete Next.js code sample from the sources.
Given the available docs:
- For Next.js, the intended combo is:
- Server:
preloadAuthQuery - Client:
usePreloadedAuthQuery[Next.js usage]
- Server:
- To avoid flashes, Better Auth’s general guidance is to hydrate the client auth state from the server (as in the SvelteKit example), but the Next.js‑specific wiring isn’t documented in these sources. [SSR optional]
So I can’t point you to an official Next.js snippet that guarantees “no flash” with usePreloadedAuthQuery; the docs just state the intent, not the full implementation details for Next.js.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@reef vessel Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@jovial kelp here is the internal code of usepreloadedauthquery: ```import { useConvexAuth, useQuery } from "convex/react";
import type { Preloaded } from "convex/react";
import { makeFunctionReference } from "convex/server";
import type { FunctionReference } from "convex/server";
import { jsonToConvex } from "convex/values";
import { useEffect, useMemo, useState } from "react";
const useConvexPreloadedQuery = <Query extends FunctionReference<"query">>(
preloadedQuery: Preloaded<Query>,
{ requireAuth = true }: { requireAuth?: boolean } = {}
): Query["_returnType"] => {
const { isLoading, isAuthenticated } = useConvexAuth();
const [preloadExpired, setPreloadExpired] = useState(false);
useEffect(() => {
if (requireAuth && !isLoading && !isAuthenticated) {
setPreloadExpired(true);
}
}, [requireAuth, isLoading, isAuthenticated]);
const args = useMemo(
() => jsonToConvex(preloadedQuery._argsJSON),
[preloadedQuery._argsJSON]
) as Query["_args"];
const preloadedResult = useMemo(
() => jsonToConvex(preloadedQuery._valueJSON),
[preloadedQuery._valueJSON]
);
const result = useQuery(
makeFunctionReference(preloadedQuery._name) as Query,
requireAuth && !isAuthenticated ? ("skip" as const) : args
);
useEffect(() => {
if (result !== undefined) {
setPreloadExpired(true);
}
}, [result]);
if (requireAuth) {
return preloadExpired ? result : preloadedResult;
}
return result === undefined ? preloadedResult : result;
};
export const usePreloadedAuthQuery = <Query extends FunctionReference<"query">>(
preloadedQuery: Preloaded<Query>
): Query["_returnType"] | null => {
const { isLoading } = useConvexAuth();
const latestData = useConvexPreloadedQuery(preloadedQuery);
const [data, setData] = useState(latestData);
useEffect(() => {
if (!isLoading) {
setData(latestData);
}
}, [latestData, isLoading]);
return data;
};```
Let me check the knowledge base for you again. One moment! :books:
You’ve basically found the reason for the flash in the implementation itself.
Key points from the code you pasted:
export const usePreloadedAuthQuery = <Query extends FunctionReference<"query">>(
preloadedQuery: Preloaded<Query>
): Query["_returnType"] | null => {
const { isLoading } = useConvexAuth();
const latestData = useConvexPreloadedQuery(preloadedQuery);
const [data, setData] = useState(latestData);
useEffect(() => {
if (!isLoading) {
setData(latestData);
}
}, [latestData, isLoading]);
return data;
};
- On the first client render,
useState(latestData)runs before effects. - During hydration,
useConvexAuth()will typically reportisLoading: truefor at least one render. - While
isLoadingistrue, theuseEffectearly‑returns and does not callsetData(latestData). - If
latestDataisundefinedon that first render (e.g. becauseuseConvexPreloadedQueryis still in the “preload vs live” transition), yourdatastate will beundefineduntilisLoadingflips tofalseand the effect runs.
That’s the flash you’re seeing: a render where data is undefined even though the preloaded value exists.
By contrast, usePreloadedQuery (from Convex core) doesn’t wrap things in this extra isLoading gate; it just returns the preloaded value immediately and then swaps to the live query result when available, so you don’t see that undefined frame. [How to use preload query with authentication]
Given this implementation:
- The flash is an inherent consequence of how
usePreloadedAuthQueryis currently written (it waits forisLoadingto be false before committinglatestDatato state). - There’s no configuration knob in the published docs to change this behavior; the Better Auth docs only state the intent (“ensures server rendered data is rendered until authentication is ready”) but not this exact internal logic. [usePreloadedAuthQuery]
So, tying this back to your earlier questions:
- Yes, if you already guarantee auth on the server and don’t need this extra
isLoadingcoordination,usePreloadedQuerywill hydrate more “synchronously” and avoid this particular flash. - But the official guidance for Better Auth + Next.js is still to use
preloadAuthQuery+usePreloadedAuthQueryfor authenticated queries. [Next.js usage]
The knowledge sources don’t give an “official” recommendation to switch back to usePreloadedQuery for authed data, nor do they document an alternative usePreloadedAuthQuery mode that avoids this flash. If this behavior is problematic for you, the next step would be to:
- Either:
- Use
usePreloadedQueryfor this specific case, accepting that you’re bypassing the auth‑aware wrapper, or - Fork / wrap
usePreloadedAuthQuerywith your own variant (e.g. dropping theisLoadinggate) in your app code.
- Use
- And report this as a UX issue with a minimal repro, since the docs already acknowledge that auth + preloaded data + flashes are an area they’re actively improving. [How to use preload query with authentication]
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@reef vessel Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@jovial kelp so deos this match what i found out myself: Hard refresh on any page → no flash. usePreloadedAuthQuery works perfectly on initial page load.
Client-side navigation back to a page → flash. Look at scenario 3 closely:
array(5) ← preloaded data (preloadExpired = false)
array(5)
array(5)
array(5)
undefined ← preloadExpired flipped to true, but live subscription has a gap
undefined
undefined
undefined
array(5) ← live subscription re-established
array(5)
The bug is in the preloaded → live subscription handoff during re-navigation. Here's what happens:
First visit to home: preloadExpired starts false, shows preloaded data, then the live query returns and sets preloadExpired = true
Navigate to idea page (home component unmounts or gets new props)
Navigate back to home: component gets fresh preloaded data, BUT preloadExpired gets set to true quickly (from a cached useQuery result), then the Convex subscription tears down and re-establishes — creating a gap where result = undefined
Since preloadExpired is true, it returns result (which is undefined) instead of preloadedResult
So this isn't an auth race condition at all — it's a subscription gap during the preloaded-to-live transition on client-side navigation. The preloadExpired flag is a one-way door that can't go back to showing preloaded data when the live subscription temporarily drops.
Let me check the knowledge base for you again. One moment! :books:
Your analysis about the kind of problem (handoff from preloaded → live query, not auth itself) matches what’s described in the Convex discussions, but the specific preloadExpired logic you’re debugging is from your local usePreloadedAuthQuery implementation and is not documented in the official sources, so I can’t fully confirm every step of your timeline from the docs alone.
What the sources do support:
- There is a known class of issues where preloaded data disappears briefly during hydration or re‑navigation, especially when auth is involved. The Convex team explicitly calls out that preloaded data “should be rendering, but then it disappears for a moment (that flash)” and that they’re working on better guarantees like “requireAuth: true” and “making SSR’d data just snap in.” [WorkOS flash discussion; How to use preload query with authentication]
- They also note that today you sometimes have to hold preloaded results in client state to render until client auth resolves, which is exactly the pattern your hook is trying to implement. [Server-side token forwarding; Better-auth example link]
- The “one‑way door” behavior you describe (once you switch off preloaded data, you don’t go back to it) is conceptually the same as what another user complained about: they want a
skip/enabled‑style control onusePreloadedQueryso they can keep showing preloaded data while the live query is in a bad state, instead of being forced intoundefined. [How to use preload query with authentication]
However:
- The official
usePreloadedQueryimplementation is documented only at a high level (“initially returns the result fetched by the Server Component. Subsequently returns the result fetched by the client”), without exposing internal flags like yourpreloadExpired. [usePreloadedQuery] - The knowledge sources don’t include the internal code of
usePreloadedAuthQuery, so I can’t verify from docs that the exact sequence you logged (cacheduseQueryresult, subscription tear‑down, gap ofundefined) is how Convex’s own auth‑aware hook behaves.
So, within the limits of the docs:
- Yes, your high‑level diagnosis is aligned with known issues: the flash is about the transition from preloaded → live subscription on client navigation, not about initial auth on a hard refresh.
- The specific mechanics you’ve traced (the
preloadExpiredflag being a one‑way door that exposes a subscription gap asundefined) come from your local implementation and aren’t described in the official materials, so I can’t definitively confirm every step from the knowledge sources.