npm install @convex-dev/prosemirror-sync
Add a collaborative editor that syncs to the cloud. With this component, users can edit the same document in multiple tabs or devices, and the changes will be synced to the cloud. The data lives in your Convex database, and can be stored alongside the rest of your app's data.
Just configure your editor features, add this component to your Convex backend,
and use the useTiptapSync
React hook.
Read this Stack post for more details.
Example usage, see below for more details:
function CollaborativeEditor() {
const sync = useTiptapSync(api.prosemirrorSync, "some-id");
return sync.isLoading ? (
<p>Loading...</p>
) : sync.initialContent !== null ? (
<EditorProvider
content={sync.initialContent}
extensions={[...extensions, sync.extension]}
>
<EditorContent editor={null} />
</EditorProvider>
) : (
<button onClick={() => sync.create(EMPTY_DOC)}>Create document</button>
);
}
Features:
Coming soon:
sessionStorage
and sync when back online (only for active browser tab).
localStorage
so new tabs
can see and edit documents offline (but won't see edits from other tabs
until they're back online).Future that could be added later:
Missing features that aren't currently planned:
Found a bug? Feature request? File it here.
You'll need an existing Convex project to use the component. Convex is a hosted backend platform, including a database, serverless functions, and a ton more you can learn about here.
Run npm create convex
or follow any of the quickstarts to set one up.
Install the component package:
npm install @convex-dev/prosemirror-sync
Create a convex.config.ts
file in your app's convex/
folder and install the component by calling use
:
// convex/convex.config.ts
import { defineApp } from "convex/server";
import prosemirrorSync from "@convex-dev/prosemirror-sync/convex.config";
const app = defineApp();
app.use(prosemirrorSync);
export default app;
To use the component, you expose the API in a file in your convex/
folder, and
use the useTiptapSync
hook in your React components, passing in a reference to
the API you defined. For this example, we'll create the API in
convex/example.ts
.
// convex/example.ts
import { components } from "./_generated/api";
import { ProsemirrorSync } from "@convex-dev/prosemirror-sync";
const prosemirrorSync = new ProsemirrorSync(components.prosemirrorSync);
export const {
getSnapshot,
submitSnapshot,
latestVersion,
getSteps,
submitSteps,
} = prosemirrorSync.syncApi({
// ...
});
In your React components, you can use the useTiptapSync
hook to fetch the
initial document and keep it in sync via a Tiptap extension. Note: This
requires a
ConvexProvider
to be in the component tree.
// src/MyComponent.tsx
import { useTiptapSync } from "@convex-dev/prosemirror-sync/tiptap";
import { EditorContent, EditorProvider } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { api } from "../convex/_generated/api";
function MyComponent() {
const sync = useTiptapSync(api.example, "some-id");
return sync.isLoading ? (
<p>Loading...</p>
) : sync.initialContent !== null ? (
<EditorProvider
content={sync.initialContent}
extensions={[StarterKit, sync.extension]}
>
<EditorContent editor={null} />
</EditorProvider>
) : (
<button onClick={() => sync.create({ type: "doc", content: [] })}>
Create document
</button>
);
}
See a working example in example.ts and App.tsx.
The snapshot debounce interval is set to one second by default.
You can specify a different interval with the snapshotDebounceMs
option when
calling useTiptapSync
.
A snapshot won't be sent until both of these are true:
There can be races, but since each client will submit the snapshot for their own change, they won't conflict with each other and are safe to apply.
You can create a new document from the client by calling sync.create(content)
, or on the server by calling prosemirrorSync.create(ctx, id, content)
.
The content should be a JSON object matching the Schema.
For client-side document creation:
!sync.isLoading
), you can choose to call it while offline with a newly
created ID to start editing a new document before you reconnect.useTiptapSync
) while online, it won't
sync.