#usePreloadedAuthQuery causes UI flash that usePreloadedQuery doesn't

29 messages · Page 1 of 1 (latest)

jovial kelpBOT
#

Hi @reef vessel. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

You’re running into a known trade‑off between “auth‑aware” hydration and “raw” hydration.

From the Better Auth docs:

  • usePreloadedQuery simply hydrates from the server‑rendered Preloaded payload and then switches to the live client query. [usePreloadedQuery]
  • usePreloadedAuthQuery is 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:

usePreloadedAuthQuery replaces usePreloadedQuery as 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.

reef vessel
#

@jovial kelp would you remmoned instead using usepreloadedquery?

jovial kelpBOT
#

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:

  • usePreloadedAuthQuery is intended as a drop‑in replacement for usePreloadedQuery that “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 usePreloadedQuery by 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.

reef vessel
#

@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."

jovial kelpBOT
#

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:

  • usePreloadedAuthQuery exists 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 isLoading is false and isAuthenticated is 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”).
  • usePreloadedAuthQuery is 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.

reef vessel
#

@jovial kelp but im using nextjs

jovial kelpBOT
#

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 preloadAuthQuery on the server and usePreloadedAuthQuery in client components. [Next.js usage]
  • usePreloadedAuthQuery is intended as a drop‑in replacement for usePreloadedQuery that “ensures server rendered data is rendered until authentication is ready.” [usePreloadedAuthQuery]

Separately, the SvelteKit guide shows the general Better Auth pattern to avoid flashes:

  1. Get auth state on the server (getAuthState(createAuth, cookies)).
  2. 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]
  • 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.

reef vessel
#

@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;
};```

jovial kelpBOT
#

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 report isLoading: true for at least one render.
  • While isLoading is true, the useEffect early‑returns and does not call setData(latestData).
  • If latestData is undefined on that first render (e.g. because useConvexPreloadedQuery is still in the “preload vs live” transition), your data state will be undefined until isLoading flips to false and 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 usePreloadedAuthQuery is currently written (it waits for isLoading to be false before committing latestData to 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 isLoading coordination, usePreloadedQuery will hydrate more “synchronously” and avoid this particular flash.
  • But the official guidance for Better Auth + Next.js is still to use preloadAuthQuery + usePreloadedAuthQuery for 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 usePreloadedQuery for this specific case, accepting that you’re bypassing the auth‑aware wrapper, or
    • Fork / wrap usePreloadedAuthQuery with your own variant (e.g. dropping the isLoading gate) in your app code.
  • 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.

reef vessel
#

@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.

jovial kelpBOT
#

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 on usePreloadedQuery so they can keep showing preloaded data while the live query is in a bad state, instead of being forced into undefined. [How to use preload query with authentication]

However:

#
  • The official usePreloadedQuery implementation 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 your preloadExpired. [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 (cached useQuery result, subscription tear‑down, gap of undefined) 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 preloadExpired flag being a one‑way door that exposes a subscription gap as undefined) 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.

Given that, your conclusion that “this isn’t an auth race, it’s a subscription gap during the preloaded‑to‑live transition, and the one‑way preloadExpired flag makes it visible” is reasonable and consistent with the kinds of flashes the Convex team acknowledges, but it goes beyond what’s explicitly documented.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: