MrUnderhill
MrUnderhill3w ago

Query data from TanStack Router loader while using Convex Auth

Hey there! I am wondering if anyone has had success making a query inside a TanStack router loader while using Convex auth. Loaders are executed outside of React, so hooks are a no go. I can’t seem to find docs or figure out a way to use the client directly with auth. Thanks for the help!
2 Replies
Convex Bot
Convex Bot3w ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
MrUnderhill
MrUnderhillOP3w ago
Ended up solving it like this:
export function useAuthenticatedConvexQuery() {
const client = useConvex();
const authPromise = useIsAuthenticated();

return useCallback(
async <Query extends FunctionReference<"query">>(
query: Query,
args: FunctionArgs<Query>,
): Promise<FunctionReturnType<Query>> => {
// Wait for auth and check the result
const isAuthenticated = await authPromise;

if (!isAuthenticated) {
throw new Error("Not authenticated");
}

// Then make the query
return await client.query(query, args);
},
[client, authPromise],
);
}

export function useIsAuthenticated() {
const { isAuthenticated, isLoading } = useConvexAuth();

// Create a stable promise that resolves when auth is determined
const authPromiseRef = useRef<{
promise: Promise<boolean>;
resolve: ((value: boolean) => void) | null;
} | null>(null);

// Create the promise if it doesn't exist
if (!authPromiseRef.current) {
let resolver: (value: boolean) => void;
const promise = new Promise<boolean>((resolve) => {
resolver = resolve;
});
authPromiseRef.current = { promise, resolve: resolver! };
}

// Resolve when auth state is determined (not loading anymore)
useEffect(() => {
if (!isLoading && authPromiseRef.current?.resolve) {
authPromiseRef.current.resolve(isAuthenticated);
authPromiseRef.current.resolve = null; // Only resolve once
}
}, [isAuthenticated, isLoading]);

return authPromiseRef.current.promise;
}

const RouteWrapper = () => {
const authenticatedQuery = useAuthenticatedConvexQuery();

return <RouterProvider context={{ authenticatedQuery }} router={router} />;
};
export function useAuthenticatedConvexQuery() {
const client = useConvex();
const authPromise = useIsAuthenticated();

return useCallback(
async <Query extends FunctionReference<"query">>(
query: Query,
args: FunctionArgs<Query>,
): Promise<FunctionReturnType<Query>> => {
// Wait for auth and check the result
const isAuthenticated = await authPromise;

if (!isAuthenticated) {
throw new Error("Not authenticated");
}

// Then make the query
return await client.query(query, args);
},
[client, authPromise],
);
}

export function useIsAuthenticated() {
const { isAuthenticated, isLoading } = useConvexAuth();

// Create a stable promise that resolves when auth is determined
const authPromiseRef = useRef<{
promise: Promise<boolean>;
resolve: ((value: boolean) => void) | null;
} | null>(null);

// Create the promise if it doesn't exist
if (!authPromiseRef.current) {
let resolver: (value: boolean) => void;
const promise = new Promise<boolean>((resolve) => {
resolver = resolve;
});
authPromiseRef.current = { promise, resolve: resolver! };
}

// Resolve when auth state is determined (not loading anymore)
useEffect(() => {
if (!isLoading && authPromiseRef.current?.resolve) {
authPromiseRef.current.resolve(isAuthenticated);
authPromiseRef.current.resolve = null; // Only resolve once
}
}, [isAuthenticated, isLoading]);

return authPromiseRef.current.promise;
}

const RouteWrapper = () => {
const authenticatedQuery = useAuthenticatedConvexQuery();

return <RouterProvider context={{ authenticatedQuery }} router={router} />;
};
Then my loader:
loader: async ({ params, context }) => {
try {
const data = await context.authenticatedQuery(api.entities.getByShortId, {
shortId: params.entityId,
});

return { data, error: null };
} catch (error) {
return { data: null, error: error.message };
}
},
loader: async ({ params, context }) => {
try {
const data = await context.authenticatedQuery(api.entities.getByShortId, {
shortId: params.entityId,
});

return { data, error: null };
} catch (error) {
return { data: null, error: error.message };
}
},
Works great! Definitely still open if there is a different way anyone would recommend doing it.

Did you find this page helpful?