Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions npm-packages/convex/src/browser/query_options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@
*/
import type { FunctionArgs, FunctionReference } from "../server/api.js";

// TODO if this type can encompass all use cases we can add not requiring args for queries
// that don't take arguments. Goal would be that queryOptions allows leaving out args,
// but queryOptions returns an object that always contains args. Helpers, "middleware,"
// anything that intercepts these arguments
/**
* Query options.
*/
export type ConvexQueryOptions<Query extends FunctionReference<"query">> = {
export type QueryOptions<Query extends FunctionReference<"query">> = {
/**
* The query function to run.
*/
query: Query;
/**
* The arguments to the query function.
*/
args: FunctionArgs<Query>;
extendSubscriptionFor?: number;
};

// This helper helps more once we have more inference happening.
export function convexQueryOptions<Query extends FunctionReference<"query">>(
options: ConvexQueryOptions<Query>,
): ConvexQueryOptions<Query> {
options: QueryOptions<Query>,
) {
return options;
}
188 changes: 175 additions & 13 deletions npm-packages/convex/src/react/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ import {
instantiateNoopLogger,
Logger,
} from "../browser/logging.js";
import { ConvexQueryOptions } from "../browser/query_options.js";
import { LoadMoreOfPaginatedQuery } from "../browser/sync/pagination.js";
import {
PaginatedQueryClient,
ExtendedTransition,
} from "../browser/sync/paginated_query_client.js";
import type { QueryOptions } from "../browser/query_options.js";
import type { Preloaded } from "./hydration.js";
import { parsePreloaded } from "./preloaded.js";

// When no arguments are passed, extend subscriptions (for APIs that do this by default)
// for this amount after the subscription would otherwise be dropped.
Expand Down Expand Up @@ -537,7 +539,7 @@ export class ConvexReactClient {
* an optional extendSubscriptionFor for how long to subscribe to the query.
*/
prewarmQuery<Query extends FunctionReference<"query">>(
queryOptions: ConvexQueryOptions<Query> & {
queryOptions: QueryOptions<Query> & {
extendSubscriptionFor?: number;
},
) {
Expand Down Expand Up @@ -801,6 +803,67 @@ export type OptionalRestArgsOrSkip<FuncRef extends FunctionReference<any>> =
? [args?: EmptyObject | "skip"]
: [args: FuncRef["_args"] | "skip"];

/**
* Options for the object-based {@link useQuery} overload.
*
* @public
*/
export type UseQueryOptions<Query extends FunctionReference<"query">> =
QueryOptions<Query> & {
/**
* Whether to throw an error if the query fails.
* If false, the error will be returned in the `error` field.
* @defaultValue false
*/
throwOnError?: boolean;
/**
* An initial value to use before the query result is available.
* @defaultValue undefined
*/
initialValue?: Query["_returnType"];
};

/**
* Options for the object-based {@link useQuery} overload with a preloaded query.
*
* @public
*/
export type UseQueryPreloadedOptions<Query extends FunctionReference<"query">> =
{
/**
* A preloaded query result from a Server Component.
*/
preloaded: Preloaded<Query>;
/**
* Whether to throw an error if the query fails.
* If false, the error will be returned in the `error` field.
* @defaultValue false
*/
throwOnError?: boolean;
};

/**
* Result type for the object-based {@link useQuery} overload.
*
* @public
*/
export type UseQueryResult<T> =
| {
status: "success";
data: T;
error: undefined;
}
| {
status: "error";
data: undefined;
error: Error;
}
| {
status: "loading";
data: undefined;
error: undefined;
};

/**
* Load a reactive query within a React component.
*
Expand All @@ -820,20 +883,83 @@ export type OptionalRestArgsOrSkip<FuncRef extends FunctionReference<any>> =
export function useQuery<Query extends FunctionReference<"query">>(
query: Query,
...args: OptionalRestArgsOrSkip<Query>
): Query["_returnType"] | undefined {
const skip = args[0] === "skip";
const argsObject = args[0] === "skip" ? {} : parseArgs(args[0]);
): Query["_returnType"] | undefined;

/**
* Load a reactive query within a React component using an options object.
*
* This overload returns an object with `status`, `error`, and `value` fields
* instead of throwing errors or returning undefined.
*
* This React hook contains internal state that will cause a rerender
* whenever the query result changes.
*
* Throws an error if not used under {@link ConvexProvider}.
*
* @param options - An options object or the string "skip" to skip the query.
* @returns An object with `status`, `error`, and `value` fields.
*
* @public
*/
export function useQuery<Query extends FunctionReference<"query">>(
options: UseQueryOptions<Query> | UseQueryPreloadedOptions<Query> | "skip",
): UseQueryResult<Query["_returnType"]>;

const queryReference =
typeof query === "string"
? makeFunctionReference<"query", any, any>(query)
: query;
export function useQuery<Query extends FunctionReference<"query">>(
queryOrOptions:
| Query
| UseQueryOptions<Query>
| UseQueryPreloadedOptions<Query>
| "skip",
...args: OptionalRestArgsOrSkip<Query>
): Query["_returnType"] | undefined | UseQueryResult<Query["_returnType"]> {
const isObjectOptions =
typeof queryOrOptions === "object" &&
queryOrOptions !== null &&
("query" in queryOrOptions || "preloaded" in queryOrOptions);
const isObjectSkip = queryOrOptions === "skip";
const isLegacy = !isObjectOptions && !isObjectSkip;
const legacySkip = isLegacy && args[0] === "skip";
const isObjectReturn = isObjectOptions || isObjectSkip;

let queryReference: Query | undefined;
let argsObject: Record<string, Value> = {};
let throwOnError = false;
let initialValue: Query["_returnType"] | undefined;
let preloadedResult: Query["_returnType"] | undefined;

if (isObjectOptions) {
if ("preloaded" in queryOrOptions) {
const parsed = parsePreloaded(queryOrOptions.preloaded);
queryReference = parsed.queryReference;
argsObject = parsed.argsObject;
preloadedResult = parsed.preloadedResult;
throwOnError = queryOrOptions.throwOnError ?? false;
} else {
const query = queryOrOptions.query;
queryReference =
typeof query === "string"
? (makeFunctionReference<"query", any, any>(query) as Query)
: query;
argsObject = queryOrOptions.args ?? ({} as Record<string, Value>);
throwOnError = queryOrOptions.throwOnError ?? false;
initialValue = queryOrOptions.initialValue;
}
} else if (isLegacy) {
const query = queryOrOptions as Query;
queryReference =
typeof query === "string"
? (makeFunctionReference<"query", any, any>(query) as Query)
: query;
argsObject = legacySkip ? {} : parseArgs(args[0] as Query["_args"]);
}

const queryName = getFunctionName(queryReference);
const skip = isObjectSkip || legacySkip;
const queryName = queryReference ? getFunctionName(queryReference) : "";

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

const results = useQueries(queries);
const result = results["query"];

if (!isObjectReturn) {
if (result instanceof Error) {
throw result;
}
return result;
}

if (result instanceof Error) {
throw result;
if (throwOnError) {
throw result;
}
return {
status: "error",
data: undefined,
error: result,
} satisfies UseQueryResult<Query["_returnType"]>;
}

if (result === undefined) {
const fallbackValue = preloadedResult ?? initialValue;
if (fallbackValue !== undefined) {
return {
status: "success",
data: fallbackValue,
error: undefined,
} satisfies UseQueryResult<Query["_returnType"]>;
}
return {
status: "loading",
data: undefined,
error: undefined,
} satisfies UseQueryResult<Query["_returnType"]>;
}
return result;

return {
status: "success",
data: result,
error: undefined,
} satisfies UseQueryResult<Query["_returnType"]>;
}

/**
Expand Down
21 changes: 7 additions & 14 deletions npm-packages/convex/src/react/hydration.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo } from "react";
import { useQuery } from "../react/client.js";
import { FunctionReference, makeFunctionReference } from "../server/api.js";
import { jsonToConvex } from "../values/index.js";
import { FunctionReference } from "../server/api.js";
import { parsePreloaded } from "./preloaded.js";

/**
* The preloaded query payload, which should be passed to a client component
Expand Down Expand Up @@ -34,17 +34,10 @@ export type Preloaded<Query extends FunctionReference<"query">> = {
export function usePreloadedQuery<Query extends FunctionReference<"query">>(
preloadedQuery: Preloaded<Query>,
): Query["_returnType"] {
const args = useMemo(
() => jsonToConvex(preloadedQuery._argsJSON),
[preloadedQuery._argsJSON],
) as Query["_args"];
const preloadedResult = useMemo(
() => jsonToConvex(preloadedQuery._valueJSON),
[preloadedQuery._valueJSON],
const parsed = useMemo(
() => parsePreloaded(preloadedQuery),
[preloadedQuery],
);
const result = useQuery(
makeFunctionReference(preloadedQuery._name) as Query,
args,
);
return result === undefined ? preloadedResult : result;
const result = useQuery(parsed.queryReference, parsed.argsObject);
return result === undefined ? parsed.preloadedResult : result;
}
1 change: 1 addition & 0 deletions npm-packages/convex/src/react/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export {
type MutationOptions,
type ConvexReactClientOptions,
type OptionalRestArgsOrSkip,
type UseQueryResult,
ConvexReactClient,
useConvex,
ConvexProvider,
Expand Down
29 changes: 29 additions & 0 deletions npm-packages/convex/src/react/preloaded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
FunctionArgs,
FunctionReference,
makeFunctionReference,
} from "../server/api.js";
import { jsonToConvex } from "../values/index.js";
import type { Preloaded } from "./hydration.js";

/**
* Parse a preloaded query payload into its constituent parts.
*
* This is a hook-free helper that can be used by both `useQuery` and
* `usePreloadedQuery` to avoid duplicating the parsing logic.
*
* @internal
*/
export function parsePreloaded<Query extends FunctionReference<"query">>(
preloaded: Preloaded<Query>,
): {
queryReference: Query;
argsObject: FunctionArgs<Query>;
preloadedResult: Query["_returnType"];
} {
return {
queryReference: makeFunctionReference(preloaded._name) as Query,
argsObject: jsonToConvex(preloaded._argsJSON) as FunctionArgs<Query>,
preloadedResult: jsonToConvex(preloaded._valueJSON) as Query["_returnType"],
};
}
Loading
Loading