Skip to content

Commit 99d6643

Browse files
committed
useQuery via object
1 parent 8fe2c47 commit 99d6643

File tree

4 files changed

+241
-25
lines changed

4 files changed

+241
-25
lines changed

npm-packages/convex/src/react/client.ts

Lines changed: 202 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import {
2828
Logger,
2929
} from "../browser/logging.js";
3030
import { ConvexQueryOptions } from "../browser/query_options.js";
31+
import type { Preloaded } from "./hydration.js";
32+
import { parsePreloaded } from "./preloaded_utils.js";
3133

3234
// When no arguments are passed, extend subscriptions (for APIs that do this by default)
3335
// for this amount after the subscription would otherwise be dropped.
@@ -642,6 +644,95 @@ export type OptionalRestArgsOrSkip<FuncRef extends FunctionReference<any>> =
642644
? [args?: EmptyObject | "skip"]
643645
: [args: FuncRef["_args"] | "skip"];
644646

647+
/**
648+
* Options for the object-based {@link useQuery} overload.
649+
*
650+
* @public
651+
*/
652+
export type UseQueryOptions<Query extends FunctionReference<"query">> = {
653+
/**
654+
* The query function to run.
655+
*/
656+
query: Query;
657+
/**
658+
* Whether to throw an error if the query fails.
659+
* If false, the error will be returned in the `error` field.
660+
* @defaultValue false
661+
*/
662+
throwOnError?: boolean;
663+
/**
664+
* An initial value to use before the query result is available.
665+
* @defaultValue undefined
666+
*/
667+
initialValue?: Query["_returnType"];
668+
/**
669+
* When true, the query will not be subscribed and it will behave the same as
670+
* if it was loading.
671+
* @defaultValue false
672+
*/
673+
skip?: boolean;
674+
} & (FunctionArgs<Query> extends EmptyObject
675+
? {
676+
/**
677+
* The arguments to the query function.
678+
* Optional for queries with no arguments.
679+
*/
680+
args?: FunctionArgs<Query>;
681+
}
682+
: {
683+
/**
684+
* The arguments to the query function.
685+
*/
686+
args: FunctionArgs<Query>;
687+
});
688+
689+
/**
690+
* Options for the object-based {@link useQuery} overload with a preloaded query.
691+
*
692+
* @public
693+
*/
694+
export type UseQueryPreloadedOptions<Query extends FunctionReference<"query">> =
695+
{
696+
/**
697+
* A preloaded query result from a Server Component.
698+
*/
699+
preloaded: Preloaded<Query>;
700+
/**
701+
* Whether to throw an error if the query fails.
702+
* If false, the error will be returned in the `error` field.
703+
* @defaultValue false
704+
*/
705+
throwOnError?: boolean;
706+
/**
707+
* When true, the query will not be subscribed and it will behave the same as
708+
* if it was loading.
709+
* @defaultValue false
710+
*/
711+
skip?: boolean;
712+
};
713+
714+
/**
715+
* Result type for the object-based {@link useQuery} overload.
716+
*
717+
* @public
718+
*/
719+
export type UseQueryResult<T> =
720+
| {
721+
status: "success";
722+
value: T;
723+
error: undefined;
724+
}
725+
| {
726+
status: "error";
727+
value: undefined;
728+
error: Error;
729+
}
730+
| {
731+
status: "loading";
732+
value: undefined;
733+
error: undefined;
734+
};
735+
645736
/**
646737
* Load a reactive query within a React component.
647738
*
@@ -661,20 +752,84 @@ export type OptionalRestArgsOrSkip<FuncRef extends FunctionReference<any>> =
661752
export function useQuery<Query extends FunctionReference<"query">>(
662753
query: Query,
663754
...args: OptionalRestArgsOrSkip<Query>
664-
): Query["_returnType"] | undefined {
665-
const skip = args[0] === "skip";
666-
const argsObject = args[0] === "skip" ? {} : parseArgs(args[0]);
755+
): Query["_returnType"] | undefined;
667756

668-
const queryReference =
669-
typeof query === "string"
670-
? makeFunctionReference<"query", any, any>(query)
671-
: query;
757+
/**
758+
* Load a reactive query within a React component using an options object.
759+
*
760+
* This overload returns an object with `status`, `error`, and `value` fields
761+
* instead of throwing errors or returning undefined.
762+
*
763+
* This React hook contains internal state that will cause a rerender
764+
* whenever the query result changes.
765+
*
766+
* Throws an error if not used under {@link ConvexProvider}.
767+
*
768+
* @param options - An options object or the string "skip" to skip the query.
769+
* @returns An object with `status`, `error`, and `value` fields.
770+
*
771+
* @public
772+
*/
773+
export function useQuery<Query extends FunctionReference<"query">>(
774+
options: UseQueryOptions<Query> | UseQueryPreloadedOptions<Query> | "skip",
775+
): UseQueryResult<Query["_returnType"]>;
672776

673-
const queryName = getFunctionName(queryReference);
777+
export function useQuery<Query extends FunctionReference<"query">>(
778+
queryOrOptions:
779+
| Query
780+
| UseQueryOptions<Query>
781+
| UseQueryPreloadedOptions<Query>
782+
| "skip",
783+
...args: OptionalRestArgsOrSkip<Query>
784+
): Query["_returnType"] | undefined | UseQueryResult<Query["_returnType"]> {
785+
const isObjectOptions =
786+
typeof queryOrOptions === "object" &&
787+
queryOrOptions !== null &&
788+
("query" in queryOrOptions || "preloaded" in queryOrOptions);
789+
const isObjectSkip =
790+
queryOrOptions === "skip" || (isObjectOptions && !!queryOrOptions.skip);
791+
const isLegacy = !isObjectOptions && !isObjectSkip;
792+
const legacySkip = isLegacy && args[0] === "skip";
793+
const isObjectReturn = isObjectOptions || isObjectSkip;
794+
795+
let queryReference: Query | undefined;
796+
let argsObject: Record<string, Value> = {};
797+
let throwOnError = false;
798+
let initialValue: Query["_returnType"] | undefined;
799+
let preloadedResult: Query["_returnType"] | undefined;
800+
801+
if (isObjectOptions) {
802+
if ("preloaded" in queryOrOptions) {
803+
const parsed = parsePreloaded(queryOrOptions.preloaded);
804+
queryReference = parsed.queryReference;
805+
argsObject = parsed.argsObject;
806+
preloadedResult = parsed.preloadedResult;
807+
throwOnError = queryOrOptions.throwOnError ?? false;
808+
} else {
809+
const query = queryOrOptions.query;
810+
queryReference =
811+
typeof query === "string"
812+
? (makeFunctionReference<"query", any, any>(query) as Query)
813+
: query;
814+
argsObject = queryOrOptions.args ?? ({} as Record<string, Value>);
815+
throwOnError = queryOrOptions.throwOnError ?? false;
816+
initialValue = queryOrOptions.initialValue;
817+
}
818+
} else if (isLegacy) {
819+
const query = queryOrOptions as Query;
820+
queryReference =
821+
typeof query === "string"
822+
? (makeFunctionReference<"query", any, any>(query) as Query)
823+
: query;
824+
argsObject = legacySkip ? {} : parseArgs(args[0] as Query["_args"]);
825+
}
826+
827+
const skip = isObjectSkip || legacySkip;
828+
const queryName = queryReference ? getFunctionName(queryReference) : "";
674829

675830
const queries = useMemo(
676831
() =>
677-
skip
832+
skip || !queryReference
678833
? ({} as RequestForQueries)
679834
: { query: { query: queryReference, args: argsObject } },
680835
// Stringify args so args that are semantically the same don't trigger a
@@ -685,10 +840,46 @@ export function useQuery<Query extends FunctionReference<"query">>(
685840

686841
const results = useQueries(queries);
687842
const result = results["query"];
843+
844+
if (!isObjectReturn) {
845+
if (result instanceof Error) {
846+
throw result;
847+
}
848+
return result;
849+
}
850+
688851
if (result instanceof Error) {
689-
throw result;
852+
if (throwOnError) {
853+
throw result;
854+
}
855+
return {
856+
status: "error",
857+
value: undefined,
858+
error: result,
859+
} satisfies UseQueryResult<Query["_returnType"]>;
690860
}
691-
return result;
861+
862+
if (result === undefined) {
863+
const fallbackValue = preloadedResult ?? initialValue;
864+
if (fallbackValue !== undefined) {
865+
return {
866+
status: "success",
867+
value: fallbackValue,
868+
error: undefined,
869+
} satisfies UseQueryResult<Query["_returnType"]>;
870+
}
871+
return {
872+
status: "loading",
873+
value: undefined,
874+
error: undefined,
875+
} satisfies UseQueryResult<Query["_returnType"]>;
876+
}
877+
878+
return {
879+
status: "success",
880+
value: result,
881+
error: undefined,
882+
} satisfies UseQueryResult<Query["_returnType"]>;
692883
}
693884

694885
/**

npm-packages/convex/src/react/hydration.tsx

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useMemo } from "react";
22
import { useQuery } from "../react/client.js";
3-
import { FunctionReference, makeFunctionReference } from "../server/api.js";
4-
import { jsonToConvex } from "../values/index.js";
3+
import { FunctionReference } from "../server/api.js";
4+
import { parsePreloaded } from "./preloaded_utils.js";
55

66
/**
77
* The preloaded query payload, which should be passed to a client component
@@ -34,17 +34,10 @@ export type Preloaded<Query extends FunctionReference<"query">> = {
3434
export function usePreloadedQuery<Query extends FunctionReference<"query">>(
3535
preloadedQuery: Preloaded<Query>,
3636
): Query["_returnType"] {
37-
const args = useMemo(
38-
() => jsonToConvex(preloadedQuery._argsJSON),
39-
[preloadedQuery._argsJSON],
40-
) as Query["_args"];
41-
const preloadedResult = useMemo(
42-
() => jsonToConvex(preloadedQuery._valueJSON),
43-
[preloadedQuery._valueJSON],
37+
const parsed = useMemo(
38+
() => parsePreloaded(preloadedQuery),
39+
[preloadedQuery],
4440
);
45-
const result = useQuery(
46-
makeFunctionReference(preloadedQuery._name) as Query,
47-
args,
48-
);
49-
return result === undefined ? preloadedResult : result;
41+
const result = useQuery(parsed.queryReference, parsed.argsObject);
42+
return result === undefined ? parsed.preloadedResult : result;
5043
}

npm-packages/convex/src/react/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ export {
7676
type MutationOptions,
7777
type ConvexReactClientOptions,
7878
type OptionalRestArgsOrSkip,
79+
type UseQueryOptions,
80+
type UseQueryPreloadedOptions,
81+
type UseQueryResult,
7982
ConvexReactClient,
8083
useConvex,
8184
ConvexProvider,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {
2+
FunctionArgs,
3+
FunctionReference,
4+
makeFunctionReference,
5+
} from "../server/api.js";
6+
import { jsonToConvex } from "../values/index.js";
7+
import type { Preloaded } from "./hydration.js";
8+
9+
/**
10+
* Parse a preloaded query payload into its constituent parts.
11+
*
12+
* This is a hook-free helper that can be used by both `useQuery` and
13+
* `usePreloadedQuery` to avoid duplicating the parsing logic.
14+
*
15+
* @internal
16+
*/
17+
export function parsePreloaded<Query extends FunctionReference<"query">>(
18+
preloaded: Preloaded<Query>,
19+
): {
20+
queryReference: Query;
21+
argsObject: FunctionArgs<Query>;
22+
preloadedResult: Query["_returnType"];
23+
} {
24+
return {
25+
queryReference: makeFunctionReference(preloaded._name) as Query,
26+
argsObject: jsonToConvex(preloaded._argsJSON) as FunctionArgs<Query>,
27+
preloadedResult: jsonToConvex(preloaded._valueJSON) as Query["_returnType"],
28+
};
29+
}

0 commit comments

Comments
 (0)