@shadow-js/convex
v1.0.4
Published
Use Convex from your app with live queries, mutations, pagination, auth and server rendering
Readme
@shadow-js/convex
Convex for ShadowJS: a provider, live queries with Suspense and server rendering, mutations and
actions with pending state, paginated queries, and auth. Every hook runs on @shadow-js/core
signals, so a query result updates only the text or node that reads it.
The package takes ConvexClient and ConvexHttpClient from convex through small structural
interfaces, so the real clients plug in unchanged and a test can pass a fake. It has no runtime
dependency on Convex beyond the convex peer dependency (>=1.31.0, the first version whose
ConvexHttpClient accepts the fetch option the server helpers use). The production checker
(npm run check:production -w @shadow-js/convex) validates a production configuration without
contacting any host.
Install
npm install @shadow-js/convex convex@shadow-js/core is a peer dependency, and the components need @shadow-js/web.
Example
Run npx convex dev once: it generates convex/_generated/api, the typed function references the
hooks take (api.tasks.list). The example below writes one reference by hand with
makeFunctionReference, which is what the generated file does under its types, and tests the
component against a fake client, so it needs no deployment.
import { useQuery } from "@shadow-js/convex";
import { For, Show } from "@shadow-js/web";
import { makeFunctionReference } from "convex/server";
export const listTasks = makeFunctionReference<"query", { done?: boolean }, string[]>("tasks:list");
export function Tasks() {
const tasks = useQuery(listTasks, { done: false });
return (
<Show when={tasks()} fallback={<p>Loading</p>}>
{(list) => (
<ul>
<For each={list()}>{(title) => <li>{title}</li>}</For>
</ul>
)}
</Show>
);
}import { ConvexProvider, type ConvexClientLike } from "@shadow-js/convex";
import { render } from "@shadow-js/testing/dom";
import { describe, expect, it } from "vitest";
import { Tasks } from "./tasks";
describe("Tasks", () => {
it("shows Loading, then the titles the server sends", () => {
const deliver: Array<(value: never) => unknown> = [];
const client: ConvexClientLike = {
onUpdate: (_query, _args, callback) => {
deliver.push(callback);
return () => undefined;
},
mutation: () => Promise.reject(new Error("not used")),
action: () => Promise.reject(new Error("not used")),
setAuth: () => undefined,
};
const view = render(() => (
<ConvexProvider client={client}>
<Tasks />
</ConvexProvider>
));
expect(view.container.textContent).toBe("Loading");
deliver[0]?.(["Write docs", "Ship it"] as never);
expect(view.container.textContent).toBe("Write docsShip it");
});
});In the application, create the browser client once and provide it near the root:
import { ConvexProvider, createConvexClient } from "@shadow-js/convex";
import { render } from "@shadow-js/web";
import { Tasks } from "./tasks";
const client = createConvexClient("https://happy-otter-123.convex.cloud");
const root = document.getElementById("app");
if (root === null) throw new Error("#app is missing");
render(
() => (
<ConvexProvider client={client}>
<Tasks />
</ConvexProvider>
),
root,
);API
The package has one entry, @shadow-js/convex.
| Export | Description |
| --- | --- |
| ConvexProvider | Makes a client available to every hook in its children. |
| createConvexClient(url, options?) | Creates the browser ConvexClient; it connects when created. |
| useConvex() | Reads the client from the nearest provider. |
| useQuery(query, args?, options?) | Subscribes to a query. Returns an accessor with status, isLoading and error. args may be an object, "skip" or an accessor. |
| useMutation(mutation, options?) | A callable with pending(), error(), reset() and withOptimisticUpdate(update). |
| usePendingMutations() | An accessor of the number of unsettled mutations under the provider. |
| ConvexProvider prop unloadGuard | Asks for confirmation before unload while a mutation is pending; false opts out. |
| useUploadFile(generateUploadUrl) | Uploads a Blob to file storage and resolves { storageId }; accessors pending, progress, error; reset(). |
| fetchQuery, fetchMutation, fetchAction (url, ref, args, { token }) | One HTTP call each for Dusk loaders and server functions; a new client per call. |
| fromConvexValidator(validator) | A Standard Schema over a v.* validator, for forms and server function input. |
| useAction(action) | A callable with pending(), error() and reset(). |
| usePaginatedQuery(query, args, { initialNumItems }) | Accessors results, status, isLoading, error and loadMore(n). |
| ConvexProviderWithAuth, useConvexAuth | Connect an auth provider to Convex and read the combined state as accessors. |
| Authenticated, Unauthenticated, AuthLoading | Show children by auth state. |
| ServerConvexProvider, preloadQuery(http, query, args?) | Server rendering: fetch queries once over HTTP and hand the values to the browser. |
| ConvexClientLike, ConvexHttpClientLike | The structural client interfaces to implement in a test. |
| QueryResult, QueryStatus, UseQueryOptions, MutationHandle, ActionHandle, PaginatedQueryResult, AuthHookState | Result and option types. |
Notes
- Function references must be the classic
FunctionReferenceshape the generatedapiuses. The_futureshape is not accepted, because convex 1.20 does not have it. ConvexClienthas noclearAuthof its own, so the auth provider calls the one on the base client behind it (client.client). Only a client with neither gets a registered fetcher that returnsnull.Authenticated,UnauthenticatedandAuthLoadingreturn a plain accessor, so a child component that calls a hook inside them does not see the provider's context. Put such children behind aShow, as the guide shows.usePaginatedQueryruns only in the browser and does not hold a Suspense boundary;useQuerydoes.
Links
- Clerk package:
clerkConvexAuth()builds theuseAuthhook forConvexProviderWithAuth. - Convex documentation: https://docs.convex.dev
Dependencies
- Runtime: none.
- Peers:
convex>=1.31.0: the client and server APIs (convex/browser,convex/server,convex/values) that the hooks wrap. - Peers:
@shadow-js/core^1.0.3: reactive state for the hooks.
Types
- Names from the root:
ActionHandle,MutationHandle,PaginatedQueryResult,ConvexClientLike. - Type tests in
test-types/are compile-time checks;npm run typecheck:typesruns them.
Support
- Node.js
>=22.12.0. ESM only: useimport;requireis not supported. - Browsers: current evergreen releases.
- Versioning: every
@shadow-js/*package releases together at one version. Upgrade them as one set. - Documentation: ShadowJS documentation.
Using, changing and contributing
- Use the package (a person or a coding agent that builds with it): this README, and SKILL.md, the short page an agent reads first.
- Change the package (fix a bug, add a feature): AGENTS.md says how the repository is laid out, how to implement and test a change, and how a pull request is reviewed. It is written for people and coding agents alike.
- Take part: CONTRIBUTING.md is the short guide; SECURITY.md is for vulnerabilities.
