convex
Version:
Client for the Convex Cloud
215 lines (204 loc) • 6.89 kB
text/typescript
/**
* Helpers for integrating Convex into Next.js applications using server rendering.
*
* This module contains:
* 1. {@link preloadQuery}, for preloading data for reactive client components.
* 2. {@link fetchQuery}, {@link fetchMutation} and {@link fetchAction} for loading and mutating Convex data
* from Next.js Server Components, Server Actions and Route Handlers.
*
* ## Usage
*
* All exported functions assume that a Convex deployment URL is set in the
* `NEXT_PUBLIC_CONVEX_URL` environment variable. `npx convex dev` will
* automatically set it during local development.
*
* ### Preloading data
*
* Preload data inside a Server Component:
*
* ```typescript
* import { preloadQuery } from "convex/nextjs";
* import { api } from "@/convex/_generated/api";
* import ClientComponent from "./ClientComponent";
*
* export async function ServerComponent() {
* const preloaded = await preloadQuery(api.foo.baz);
* return <ClientComponent preloaded={preloaded} />;
* }
* ```
*
* And pass it to a Client Component:
* ```typescript
* import { Preloaded, usePreloadedQuery } from "convex/nextjs";
* import { api } from "@/convex/_generated/api";
*
* export function ClientComponent(props: {
* preloaded: Preloaded<typeof api.foo.baz>;
* }) {
* const data = await usePreloadedQuery(props.preloaded);
* // render `data`...
* }
* ```
*
* @module
*/
import { ConvexHttpClient } from "../browser/index.js";
import { validateDeploymentUrl } from "../common/index.js";
import { Preloaded } from "../react/index.js";
import {
ArgsAndOptions,
FunctionReference,
FunctionReturnType,
getFunctionName,
} from "../server/index.js";
import { convexToJson, jsonToConvex } from "../values/index.js";
/**
* Options to {@link preloadQuery}, {@link fetchQuery}, {@link fetchMutation} and {@link fetchAction}.
*/
export type NextjsOptions = {
/**
* The JWT-encoded OpenID Connect authentication token to use for the function call.
*/
token?: string;
/**
* The URL of the Convex deployment to use for the function call.
* Defaults to `process.env.NEXT_PUBLIC_CONVEX_URL`.
*/
url?: string;
/**
* @internal
*/
adminToken?: string;
/**
* Skip validating that the Convex deployment URL looks like
* `https://happy-animal-123.convex.cloud` or localhost.
*
* This can be useful if running a self-hosted Convex backend that uses a different
* URL.
*
* The default value is `false`
*/
skipConvexDeploymentUrlCheck?: boolean;
};
/**
* Execute a Convex query function and return a `Preloaded`
* payload which can be passed to {@link react.usePreloadedQuery} in a Client
* Component.
*
* @param query - a {@link server.FunctionReference} for the public query to run
* like `api.dir1.dir2.filename.func`.
* @param args - The arguments object for the query. If this is omitted,
* the arguments will be `{}`.
* @param options - A {@link NextjsOptions} options object for the query.
* @returns A promise of the `Preloaded` payload.
*/
export async function preloadQuery<Query extends FunctionReference<"query">>(
query: Query,
...args: ArgsAndOptions<Query, NextjsOptions>
): Promise<Preloaded<Query>> {
const value = await fetchQuery(query, ...args);
const preloaded = {
_name: getFunctionName(query),
_argsJSON: convexToJson(args[0] ?? {}),
_valueJSON: convexToJson(value),
};
return preloaded as any;
}
/**
* Returns the result of executing a query via {@link preloadQuery}.
*
* @param preloaded - The `Preloaded` payload returned by {@link preloadQuery}.
* @returns The query result.
*/
export function preloadedQueryResult<Query extends FunctionReference<"query">>(
preloaded: Preloaded<Query>,
): FunctionReturnType<Query> {
return jsonToConvex(preloaded._valueJSON);
}
/**
* Execute a Convex query function.
*
* @param query - a {@link server.FunctionReference} for the public query to run
* like `api.dir1.dir2.filename.func`.
* @param args - The arguments object for the query. If this is omitted,
* the arguments will be `{}`.
* @param options - A {@link NextjsOptions} options object for the query.
* @returns A promise of the query's result.
*/
export async function fetchQuery<Query extends FunctionReference<"query">>(
query: Query,
...args: ArgsAndOptions<Query, NextjsOptions>
): Promise<FunctionReturnType<Query>> {
const [fnArgs, options] = args;
const client = setupClient(options ?? {});
return client.query(query, fnArgs);
}
/**
* Execute a Convex mutation function.
*
* @param mutation - A {@link server.FunctionReference} for the public mutation
* to run like `api.dir1.dir2.filename.func`.
* @param args - The arguments object for the mutation. If this is omitted,
* the arguments will be `{}`.
* @param options - A {@link NextjsOptions} options object for the mutation.
* @returns A promise of the mutation's result.
*/
export async function fetchMutation<
Mutation extends FunctionReference<"mutation">,
>(
mutation: Mutation,
...args: ArgsAndOptions<Mutation, NextjsOptions>
): Promise<FunctionReturnType<Mutation>> {
const [fnArgs, options] = args;
const client = setupClient(options ?? {});
return client.mutation(mutation, fnArgs);
}
/**
* Execute a Convex action function.
*
* @param action - A {@link server.FunctionReference} for the public action
* to run like `api.dir1.dir2.filename.func`.
* @param args - The arguments object for the action. If this is omitted,
* the arguments will be `{}`.
* @param options - A {@link NextjsOptions} options object for the action.
* @returns A promise of the action's result.
*/
export async function fetchAction<Action extends FunctionReference<"action">>(
action: Action,
...args: ArgsAndOptions<Action, NextjsOptions>
): Promise<FunctionReturnType<Action>> {
const [fnArgs, options] = args;
const client = setupClient(options ?? {});
return client.action(action, fnArgs);
}
function setupClient(options: NextjsOptions) {
const client = new ConvexHttpClient(
getConvexUrl(options.url, options.skipConvexDeploymentUrlCheck ?? false),
);
if (options.token !== undefined) {
client.setAuth(options.token);
}
if (options.adminToken !== undefined) {
client.setAdminAuth(options.adminToken);
}
client.setFetchOptions({ cache: "no-store" });
return client;
}
function getConvexUrl(
deploymentUrl: string | undefined,
skipConvexDeploymentUrlCheck: boolean,
) {
const url = deploymentUrl ?? process.env.NEXT_PUBLIC_CONVEX_URL;
const isFromEnv = deploymentUrl === undefined;
if (typeof url !== "string") {
throw new Error(
isFromEnv
? `Environment variable NEXT_PUBLIC_CONVEX_URL is not set.`
: `Convex function called with invalid deployment address.`,
);
}
if (!skipConvexDeploymentUrlCheck) {
validateDeploymentUrl(url);
}
return url!;
}