Migration guide
Move from the official Convex JavaScript and React clients to Convex Pulse.
Convex Pulse uses the same deployment URL, generated function references, function arguments, and backend. Migration is limited to client construction, imports, call signatures, and UI state handling.
This guide maps the current official JavaScript client and React client APIs to Convex Pulse.
Package imports
| Before | After |
|---|---|
convex/browser |
convex-pulse |
convex/react |
convex-pulse/react |
ConvexClient |
ConvexPulseClient |
ConvexReactClient |
ConvexPulseReactClient |
ConvexProvider |
ConvexPulseReactProvider |
Keep convex installed because Convex Pulse uses its generated API and public types.
JavaScript and Node.js
The official ConvexClient accepts positional arguments. Convex Pulse groups operation arguments with retry, selection, deduplication, and optimistic options.
// Before
import { ConvexClient } from 'convex/browser'
const client = new ConvexClient(CONVEX_URL)
const tasks = await client.query(api.tasks.list, {})
const unsubscribe = client.onUpdate(api.tasks.list, {}, console.log)
await client.mutation(api.tasks.create, { title: 'Migrate' })
await client.action(api.tasks.format, { title: 'Migrate' })
// After
import { ConvexPulseClient } from 'convex-pulse'
const client = new ConvexPulseClient(CONVEX_URL)
const tasks = await client.query(api.tasks.list, { args: {} })
const subscription = client.onUpdate(api.tasks.list, { args: {} }, console.log)
await client.mutation(api.tasks.create, {
args: { title: 'Migrate' }
})
await client.action(api.tasks.format, {
args: { title: 'Migrate' }
})
Convex Pulse’s onUpdate returns a callable subscription with unsubscribe(), getCurrentValue(), and onError(listener). Use watchQuery when an async iterable is a better fit. Both clients expose asynchronous close() cleanup.
React setup
Rename the client and provider. The Pulse provider prop is convex rather than client.
// Before
const convex = new ConvexReactClient(CONVEX_URL)
root.render(
<ConvexProvider client={convex}>
<App />
</ConvexProvider>
)
// After
const convex = new ConvexPulseReactClient(CONVEX_URL)
root.render(
<ConvexPulseReactProvider convex={convex}>
<App />
</ConvexPulseReactProvider>
)
React queries
Move arguments into options.args. Replace the official client’s undefined-while-loading model with a discriminated result.
// Before
const tasks = useQuery(api.tasks.list, { listId })
if (tasks === undefined) return <Spinner />
return <TaskList tasks={tasks} />
// After
const tasks = useQuery(api.tasks.list, { args: { listId } })
if (tasks.status === 'pending') return <Spinner />
if (tasks.status === 'error') return <ErrorView error={tasks.error} />
if (tasks.status === 'disabled') return null
return <TaskList tasks={tasks.data} />
Replace "skip" with enabled:
// Before
useQuery(api.tasks.get, id ? { id } : 'skip')
// After
useQuery(api.tasks.get, {
args: { id: id! },
enabled: id !== undefined
})
React mutations
Calling a mutation remains the same. Configuration moves from chained helpers to the hook’s options, and the callable also exposes lifecycle state.
// Before
const createTask = useMutation(api.tasks.create).withOptimisticUpdate(
(store, args) => {
const current = store.getQuery(api.tasks.list, {})
if (current) store.setQuery(api.tasks.list, {}, [...current, args])
}
)
// After
const createTask = useMutation(api.tasks.create, {
optimistic: ({ data, optimisticId, store }) =>
store.get(api.tasks.list).append({
_id: optimisticId,
_creationTime: Date.now(),
done: false,
title: data.title
})
})
Replace manual getQuery/setQuery transforms with shape-aware operations.
Pagination
Replace the separate usePaginatedQuery hook with the normal Pulse query hook.
// Before
const { results, status, loadMore } = usePaginatedQuery(
api.tasks.listPaginated,
{ listId },
{ initialNumItems: 20 }
)
// After
const tasks = useQuery(api.tasks.listPaginated, {
args: { listId },
pagination: { initialNumItems: 20 }
})
if (tasks.status === 'success' && tasks.canLoadMore) {
tasks.loadMore(20)
}
Actions
Replace the official React hook with the Pulse lifecycle-aware hook:
// Before
const format = useAction(api.tasks.format)
await format({ title })
// After
const format = useAction(api.tasks.format)
await format({ title })
The Pulse hook also exposes status, data, error, isPending, callbacks, retries, and reset(). The JavaScript client uses .action(reference, { args: { title } }).
Authentication
Replace ConvexProviderWithAuth or provider-specific wrappers with your auth provider and pass its token fetcher to ConvexPulseReactProvider:
const fetchToken = useCallback(
({ forceRefreshToken }: { forceRefreshToken: boolean }) =>
getToken({ skipCache: forceRefreshToken }),
[getToken]
)
return (
<ConvexPulseReactProvider convex={convex} fetchToken={fetchToken}>
<App />
</ConvexPulseReactProvider>
)
You can instead pass fetchToken when constructing any live Pulse client or call client.setAuth() programmatically. Use onChange, onError, and onRefreshChange with setAuth() if the UI needs authentication state. Convex Pulse does not currently provide Authenticated, Unauthenticated, or useConvexAuth components/hooks.
Before removing old imports
Verify these differences:
- Every query handles
pending,error,success, and optionallydisabled. - Every query’s arguments are inside
args. - Conditional queries use
enabledrather than"skip". - Paginated views use
dataandcanLoadMorerather thanresultsand the official pagination status strings. - Actions use the client instance.
- Authentication state previously supplied by Convex React helpers is supplied by your auth integration.
- Each long-lived client is closed when its application or process ends.