Skip to content

Commit b6377e0

Browse files
committed
useQuery via object
1 parent 717fe22 commit b6377e0

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
@@ -38,6 +38,8 @@ import {
3838
PaginatedQueryClient,
3939
ExtendedTransition,
4040
} from "../browser/sync/paginated_query_client.js";
41+
import type { Preloaded } from "./hydration.js";
42+
import { parsePreloaded } from "./preloaded_utils.js";
4143

4244
// When no arguments are passed, extend subscriptions (for APIs that do this by default)
4345
// for this amount after the subscription would otherwise be dropped.
@@ -801,6 +803,95 @@ export type OptionalRestArgsOrSkip<FuncRef extends FunctionReference<any>> =
801803
? [args?: EmptyObject | "skip"]
802804
: [args: FuncRef["_args"] | "skip"];
803805

806+
/**
807+
* Options for the object-based {@link useQuery} overload.
808+
*
809+
* @public
810+
*/
811+
export type UseQueryOptions<Query extends FunctionReference<"query">> = {
812+
/**
813+
* The query function to run.
814+
*/
815+
query: Query;
816+
/**
817+
* Whether to throw an error if the query fails.
818+
* If false, the error will be returned in the `error` field.
819+
* @defaultValue false
820+
*/
821+
throwOnError?: boolean;
822+
/**
823+
* An initial value to use before the query result is available.
824+
* @defaultValue undefined
825+
*/
826+
initialValue?: Query["_returnType"];
827+
/**
828+
* When true, the query will not be subscribed and it will behave the same as
829+
* if it was loading.
830+
* @defaultValue false
831+
*/
832+
skip?: boolean;
833+
} & (FunctionArgs<Query> extends EmptyObject
834+
? {
835+
/**
836+
* The arguments to the query function.
837+
* Optional for queries with no arguments.
838+
*/
839+
args?: FunctionArgs<Query>;
840+
}
841+
: {
842+
/**
843+
* The arguments to the query function.
844+
*/
845+
args: FunctionArgs<Query>;
846+
});
847+
848+
/**
849+
* Options for the object-based {@link useQuery} overload with a preloaded query.
850+
*
851+
* @public
852+
*/
853+
export type UseQueryPreloadedOptions<Query extends FunctionReference<"query">> =
854+
{
855+
/**
856+
* A preloaded query result from a Server Component.
857+
*/
858+
preloaded: Preloaded<Query>;
859+
/**
860+
* Whether to throw an error if the query fails.
861+
* If false, the error will be returned in the `error` field.
862+
* @defaultValue false
863+
*/
864+
throwOnError?: boolean;
865+
/**
866+
* When true, the query will not be subscribed and it will behave the same as
867+
* if it was loading.
868+
* @defaultValue false
869+
*/
870+
skip?: boolean;
871+
};
872+
873+
/**
874+
* Result type for the object-based {@link useQuery} overload.
875+
*
876+
* @public
877+
*/
878+
export type UseQueryResult<T> =
879+
| {
880+
status: "success";
881+
value: T;
882+
error: undefined;
883+
}
884+
| {
885+
status: "error";
886+
value: undefined;
887+
error: Error;
888+
}
889+
| {
890+
status: "loading";
891+
value: undefined;
892+
error: undefined;
893+
};
894+
804895
/**
805896
* Load a reactive query within a React component.
806897
*
@@ -820,20 +911,84 @@ export type OptionalRestArgsOrSkip<FuncRef extends FunctionReference<any>> =
820911
export function useQuery<Query extends FunctionReference<"query">>(
821912
query: Query,
822913
...args: OptionalRestArgsOrSkip<Query>
823-
): Query["_returnType"] | undefined {
824-
const skip = args[0] === "skip";
825-
const argsObject = args[0] === "skip" ? {} : parseArgs(args[0]);
914+
): Query["_returnType"] | undefined;
826915

827-
const queryReference =
828-
typeof query === "string"
829-
? makeFunctionReference<"query", any, any>(query)
830-
: query;
916+
/**
917+
* Load a reactive query within a React component using an options object.
918+
*
919+
* This overload returns an object with `status`, `error`, and `value` fields
920+
* instead of throwing errors or returning undefined.
921+
*
922+
* This React hook contains internal state that will cause a rerender
923+
* whenever the query result changes.
924+
*
925+
* Throws an error if not used under {@link ConvexProvider}.
926+
*
927+
* @param options - An options object or the string "skip" to skip the query.
928+
* @returns An object with `status`, `error`, and `value` fields.
929+
*
930+
* @public
931+
*/
932+
export function useQuery<Query extends FunctionReference<"query">>(
933+
options: UseQueryOptions<Query> | UseQueryPreloadedOptions<Query> | "skip",
934+
): UseQueryResult<Query["_returnType"]>;
935+
936+
export function useQuery<Query extends FunctionReference<"query">>(
937+
queryOrOptions:
938+
| Query
939+
| UseQueryOptions<Query>
940+
| UseQueryPreloadedOptions<Query>
941+
| "skip",
942+
...args: OptionalRestArgsOrSkip<Query>
943+
): Query["_returnType"] | undefined | UseQueryResult<Query["_returnType"]> {
944+
const isObjectOptions =
945+
typeof queryOrOptions === "object" &&
946+
queryOrOptions !== null &&
947+
("query" in queryOrOptions || "preloaded" in queryOrOptions);
948+
const isObjectSkip =
949+
queryOrOptions === "skip" || (isObjectOptions && !!queryOrOptions.skip);
950+
const isLegacy = !isObjectOptions && !isObjectSkip;
951+
const legacySkip = isLegacy && args[0] === "skip";
952+
const isObjectReturn = isObjectOptions || isObjectSkip;
953+
954+
let queryReference: Query | undefined;
955+
let argsObject: Record<string, Value> = {};
956+
let throwOnError = false;
957+
let initialValue: Query["_returnType"] | undefined;
958+
let preloadedResult: Query["_returnType"] | undefined;
959+
960+
if (isObjectOptions) {
961+
if ("preloaded" in queryOrOptions) {
962+
const parsed = parsePreloaded(queryOrOptions.preloaded);
963+
queryReference = parsed.queryReference;
964+
argsObject = parsed.argsObject;
965+
preloadedResult = parsed.preloadedResult;
966+
throwOnError = queryOrOptions.throwOnError ?? false;
967+
} else {
968+
const query = queryOrOptions.query;
969+
queryReference =
970+
typeof query === "string"
971+
? (makeFunctionReference<"query", any, any>(query) as Query)
972+
: query;
973+
argsObject = queryOrOptions.args ?? ({} as Record<string, Value>);
974+
throwOnError = queryOrOptions.throwOnError ?? false;
975+
initialValue = queryOrOptions.initialValue;
976+
}
977+
} else if (isLegacy) {
978+
const query = queryOrOptions as Query;
979+
queryReference =
980+
typeof query === "string"
981+
? (makeFunctionReference<"query", any, any>(query) as Query)
982+
: query;
983+
argsObject = legacySkip ? {} : parseArgs(args[0] as Query["_args"]);
984+
}
831985

832-
const queryName = getFunctionName(queryReference);
986+
const skip = isObjectSkip || legacySkip;
987+
const queryName = queryReference ? getFunctionName(queryReference) : "";
833988

834989
const queries = useMemo(
835990
() =>
836-
skip
991+
skip || !queryReference
837992
? ({} as RequestForQueries)
838993
: { query: { query: queryReference, args: argsObject } },
839994
// Stringify args so args that are semantically the same don't trigger a
@@ -844,10 +999,46 @@ export function useQuery<Query extends FunctionReference<"query">>(
844999

8451000
const results = useQueries(queries);
8461001
const result = results["query"];
1002+
1003+
if (!isObjectReturn) {
1004+
if (result instanceof Error) {
1005+
throw result;
1006+
}
1007+
return result;
1008+
}
1009+
8471010
if (result instanceof Error) {
848-
throw result;
1011+
if (throwOnError) {
1012+
throw result;
1013+
}
1014+
return {
1015+
status: "error",
1016+
value: undefined,
1017+
error: result,
1018+
} satisfies UseQueryResult<Query["_returnType"]>;
8491019
}
850-
return result;
1020+
1021+
if (result === undefined) {
1022+
const fallbackValue = preloadedResult ?? initialValue;
1023+
if (fallbackValue !== undefined) {
1024+
return {
1025+
status: "success",
1026+
value: fallbackValue,
1027+
error: undefined,
1028+
} satisfies UseQueryResult<Query["_returnType"]>;
1029+
}
1030+
return {
1031+
status: "loading",
1032+
value: undefined,
1033+
error: undefined,
1034+
} satisfies UseQueryResult<Query["_returnType"]>;
1035+
}
1036+
1037+
return {
1038+
status: "success",
1039+
value: result,
1040+
error: undefined,
1041+
} satisfies UseQueryResult<Query["_returnType"]>;
8511042
}
8521043

8531044
/**

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
@@ -78,6 +78,9 @@ export {
7878
type MutationOptions,
7979
type ConvexReactClientOptions,
8080
type OptionalRestArgsOrSkip,
81+
type UseQueryOptions,
82+
type UseQueryPreloadedOptions,
83+
type UseQueryResult,
8184
ConvexReactClient,
8285
useConvex,
8386
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)