-
Notifications
You must be signed in to change notification settings - Fork 0
feat: wire initial Next.js app to Convex backend #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
37766e1
ca4fdf7
659e598
858bb90
2f59098
51014c7
9aa9cd7
8700783
bc428a7
85fb9ca
b4ecf44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| "use client"; | ||
|
|
||
| import { ConvexProvider, ConvexReactClient } from "convex/react"; | ||
| import type { ReactNode } from "react"; | ||
|
|
||
| let convexClient: ConvexReactClient | null = null; | ||
| let convexClientUrl: string | null = null; | ||
|
|
||
| function getConvexClient(convexUrl: string | undefined) { | ||
| if (!convexUrl || typeof window === "undefined") { | ||
| return null; | ||
| } | ||
|
|
||
| if (!convexClient || convexClientUrl !== convexUrl) { | ||
| convexClient?.close(); | ||
| convexClient = new ConvexReactClient(convexUrl); | ||
| convexClientUrl = convexUrl; | ||
| } | ||
|
|
||
| return convexClient; | ||
| } | ||
|
|
||
| export function resetConvexClientForTests() { | ||
| convexClient?.close(); | ||
| convexClient = null; | ||
| convexClientUrl = null; | ||
| } | ||
|
|
||
| export function ConvexClientProvider({ children }: { children: ReactNode }) { | ||
| const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL; | ||
| const client = getConvexClient(convexUrl); | ||
|
|
||
| if (!client) { | ||
| return <>{children}</>; | ||
| } | ||
|
|
||
| return <ConvexProvider client={client}>{children}</ConvexProvider>; | ||
| } | ||
|
Comment on lines
+29
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Side-effectful function called in render body
A light guard would make the mutation safe: // Only mutate when the effect is committed (client-side).
// If the URL cannot change at runtime this is low-risk today, but
// explicit side-effect isolation prevents surprises under concurrent features.Consider wrapping the close/create path in Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/web/src/app/convex-client-provider.tsx
Line: 29-38
Comment:
**Side-effectful function called in render body**
`getConvexClient` can call `convexClient?.close()` and `new ConvexReactClient(convexUrl)` when the URL changes. Calling these side effects directly during render — rather than inside a `useEffect` or event handler — is outside React's render-purity contract. In practice the URL is a compile-time constant so the branch that mutates the singleton only runs on the very first render, but React's concurrent renderer can invoke render functions multiple times before committing (e.g. during `startTransition` or hydration), which would incorrectly attempt to close and reopen the WebSocket client on each discarded render.
A light guard would make the mutation safe:
```ts
// Only mutate when the effect is committed (client-side).
// If the URL cannot change at runtime this is low-risk today, but
// explicit side-effect isolation prevents surprises under concurrent features.
```
Consider wrapping the close/create path in `useEffect` (keyed on `convexUrl`) inside `ConvexClientProvider` rather than calling it inline in the render body.
How can I resolve this? If you propose a fix, please make it concise. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| "use client"; | ||
|
|
||
| import { api } from "@convex/_generated/api"; | ||
| import { Component, type ReactNode, useState, useSyncExternalStore } from "react"; | ||
| import { useQuery } from "convex/react"; | ||
|
|
||
| function subscribeToClientReady() { | ||
| return () => {}; | ||
| } | ||
|
|
||
| class ConvexQueryErrorBoundary extends Component< | ||
| { children: ReactNode; onRetry: () => void }, | ||
| { hasError: boolean } | ||
| > { | ||
| state = { hasError: false }; | ||
|
|
||
| static getDerivedStateFromError() { | ||
| return { hasError: true }; | ||
| } | ||
|
|
||
| componentDidCatch(error: unknown, info: React.ErrorInfo) { | ||
| console.error("[ConvexQueryErrorBoundary] Caught backend query error:", error, info); | ||
| } | ||
|
|
||
| render() { | ||
| if (this.state.hasError) { | ||
| return ( | ||
| <div className="space-y-4"> | ||
| <div> | ||
| <p className="font-mono text-xs uppercase tracking-[0.3em] text-muted"> | ||
| Live Convex status | ||
| </p> | ||
| <h2 className="mt-3 text-2xl font-semibold tracking-[-0.03em]"> | ||
| Backend query needs attention. | ||
| </h2> | ||
| </div> | ||
|
|
||
| <div className="rounded-[1.25rem] border border-dashed border-border bg-surface px-4 py-5 text-sm leading-7 text-foreground"> | ||
| The live Convex panel hit a backend error. Check{" "} | ||
| <code className="font-mono">pnpm dev:backend:local</code> and refresh once the local | ||
| backend is healthy again. | ||
| </div> | ||
|
|
||
| <button | ||
| className="inline-flex w-fit items-center rounded-full border border-border px-4 py-2 text-sm font-medium text-foreground transition hover:bg-surface" | ||
| onClick={this.props.onRetry} | ||
| type="button" | ||
| > | ||
| Try again | ||
| </button> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return this.props.children; | ||
| } | ||
| } | ||
BASIC-BIT marked this conversation as resolved.
Show resolved
Hide resolved
BASIC-BIT marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| function ConvexRuntimeStatus({ convexUrl }: { convexUrl: string }) { | ||
| const status = useQuery(api.health.status); | ||
BASIC-BIT marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const statusHeading = status === undefined ? "Connecting to Convex..." : "The first runtime path is active."; | ||
|
|
||
| return ( | ||
| <div className="space-y-4"> | ||
| <div> | ||
| <p className="font-mono text-xs uppercase tracking-[0.3em] text-muted"> | ||
| Live Convex status | ||
| </p> | ||
| <h2 className="mt-3 text-2xl font-semibold tracking-[-0.03em]"> | ||
| {statusHeading} | ||
| </h2> | ||
BASIC-BIT marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </div> | ||
|
|
||
| <p className="text-sm leading-7 text-muted"> | ||
| This panel reads the placeholder <code className="font-mono">health:status</code>{" "} | ||
| query from the real backend bootstrap under <code className="font-mono">convex/</code>. | ||
| </p> | ||
|
|
||
| <div className="rounded-[1.25rem] border border-border bg-surface p-4"> | ||
| <p className="font-mono text-[11px] uppercase tracking-[0.28em] text-muted"> | ||
| Backend endpoint | ||
| </p> | ||
| <p className="mt-2 break-all font-mono text-sm text-foreground">{convexUrl}</p> | ||
| </div> | ||
|
|
||
| {status === undefined ? ( | ||
| <div className="rounded-[1.25rem] border border-dashed border-border bg-surface px-4 py-5"> | ||
| <p className="font-mono text-[11px] uppercase tracking-[0.28em] text-muted"> | ||
| Query state | ||
| </p> | ||
| <p className="mt-2 text-sm leading-7 text-foreground"> | ||
| Connecting to Convex and waiting for the first placeholder payload. | ||
| </p> | ||
| </div> | ||
| ) : status ? ( | ||
| <dl className="space-y-3 text-sm"> | ||
| <div className="flex items-start justify-between gap-4 border-b border-border pb-3"> | ||
| <dt className="text-muted">Status</dt> | ||
| <dd className="text-right font-medium text-emerald-700">{status.status}</dd> | ||
| </div> | ||
| <div className="flex items-start justify-between gap-4 border-b border-border pb-3"> | ||
| <dt className="text-muted">Backend</dt> | ||
| <dd className="text-right font-medium">{status.backend}</dd> | ||
| </div> | ||
| <div className="flex items-start justify-between gap-4 border-b border-border pb-3"> | ||
| <dt className="text-muted">Project</dt> | ||
| <dd className="text-right font-medium">{status.project}</dd> | ||
| </div> | ||
| <div className="flex items-start justify-between gap-4 border-b border-border pb-3"> | ||
| <dt className="text-muted">Scope</dt> | ||
| <dd className="text-right font-medium">{status.scope}</dd> | ||
| </div> | ||
| <div className="space-y-2 pt-1"> | ||
| <dt className="text-muted">Backend note</dt> | ||
| <dd className="text-sm leading-7 text-foreground">{status.note}</dd> | ||
| </div> | ||
| </dl> | ||
| ) : ( | ||
| <div className="rounded-[1.25rem] border border-dashed border-border bg-surface px-4 py-5"> | ||
| <p className="font-mono text-[11px] uppercase tracking-[0.28em] text-muted"> | ||
| Query result | ||
| </p> | ||
| <p className="mt-2 text-sm leading-7 text-foreground"> | ||
| Convex returned an empty placeholder payload. Refresh after restarting the local | ||
| backend if this sticks around. | ||
| </p> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export function ConvexRuntimePanel() { | ||
| const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL; | ||
| const isClient = useSyncExternalStore(subscribeToClientReady, () => true, () => false); | ||
| const [retryKey, setRetryKey] = useState(0); | ||
|
|
||
| if (!convexUrl) { | ||
| return ( | ||
| <div className="space-y-4"> | ||
| <div> | ||
| <p className="font-mono text-xs uppercase tracking-[0.3em] text-muted"> | ||
| Convex runtime path | ||
| </p> | ||
| <h2 className="mt-3 text-2xl font-semibold tracking-[-0.03em]"> | ||
| Waiting for local backend wiring. | ||
| </h2> | ||
| </div> | ||
|
|
||
| <p className="text-sm leading-7 text-muted"> | ||
| The app can render without a backend URL, but the live Convex read path only | ||
| turns on when the repo-root <code className="font-mono">.env.local</code>{" "} | ||
| file exists. | ||
| </p> | ||
|
|
||
| <div className="rounded-[1.25rem] border border-dashed border-border bg-surface px-4 py-5 text-sm leading-7 text-foreground"> | ||
| Run <code className="font-mono">pnpm bootstrap:backend:local</code> once, keep{" "} | ||
| <code className="font-mono">pnpm dev:backend:local</code> running, and start the web | ||
| app with <code className="font-mono">pnpm dev:web</code> from the repo root. | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (!isClient) { | ||
| return ( | ||
| <div className="space-y-4"> | ||
| <div> | ||
| <p className="font-mono text-xs uppercase tracking-[0.3em] text-muted"> | ||
| Live Convex status | ||
| </p> | ||
| <h2 className="mt-3 text-2xl font-semibold tracking-[-0.03em]"> | ||
| Connecting to Convex... | ||
| </h2> | ||
| </div> | ||
|
|
||
| <div className="rounded-[1.25rem] border border-dashed border-border bg-surface px-4 py-5 text-sm leading-7 text-foreground"> | ||
| Preparing the client-side Convex runtime. | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <ConvexQueryErrorBoundary key={retryKey} onRetry={() => setRetryKey((current) => current + 1)}> | ||
| <ConvexRuntimeStatus convexUrl={convexUrl} /> | ||
| </ConvexQueryErrorBoundary> | ||
| ); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.