A common use case for text editors is collaborative editing, and the Slate editor was designed with this in mind. You can enable multiplayer editing with Yjs, a network-agnostic CRDT implementation that allows you to share data among connected users. Because Yjs is network-agnostic, each project requires a communication provider set up on the back end to link users together.
In this guide, we'll show you how to set up a collaborative Slate editor using a Yjs provider. We'll also be adding slate-yjs which allows you to add multiplayer features to Slate, such as live cursors.
Yjs is network-agnostic, which means each Yjs provider is set up in a slightly different way. For example @liveblocks/yjs is fully-hosted, whereas others such as y-websocket require you to host your own WebSocket server. Because of this, we'll use code snippets that work for each provider, without going into too much detail about setting up the provider itself.
This is how to connect to a collaborative Yjs document, ready to be used in your Slate editor.
import { useEffect, useMemo, useState } from'react'import { createEditor, Editor, Transforms } from'slate'import { Editable, Slate, withReact } from'slate-react'import*as Y from'yjs'constinitialValue= { children: [{ text:'' }],}exportconstCollaborativeEditor= () => {const [connected,setConnected] =useState(false)const [sharedType,setSharedType] =useState()const [provider,setProvider] =useState()// Set up your Yjs provider and documentuseEffect(() => {constyDoc=newY.Doc()constsharedDoc=yDoc.get('slate',Y.XmlText)// Set up your Yjs provider. This line of code is different for each provider.constyProvider=newYjsProvider(/* ... */)yProvider.on('sync', setConnected)setSharedType(sharedDoc)setProvider(yProvider)return () => {yDoc?.destroy()yProvider?.off('sync', setConnected)yProvider?.destroy() } }, [])if (!connected ||!sharedType ||!provider) {return <div>Loading…</div> }return <SlateEditor />}constSlateEditor= () => {const [editor] =useState(() =>withReact(createEditor()))return ( <Slateeditor={editor} initialValue={initialValue}> <Editable /> </Slate> )}
After setting up your Yjs document like this, you can then link it your editor by passing down sharedType, which contains the multiplayer text, and by using functions from slate-yjs. We're also passing down provider which will be helpful later.
import { useEffect, useMemo, useState } from'react'import { createEditor, Editor, Transforms } from'slate'import { Editable, Slate, withReact } from'slate-react'import { withYjs, YjsEditor } from'@slate-yjs/core'import*as Y from'yjs'constinitialValue= { children: [{ text:'' }],}exportconstCollaborativeEditor= () => {const [connected,setConnected] =useState(false)const [sharedType,setSharedType] =useState()const [provider,setProvider] =useState()// Connect to your Yjs provider and documentuseEffect(() => {constyDoc=newY.Doc()constsharedDoc=yDoc.get('slate',Y.XmlText)// Set up your Yjs provider. This line of code is different for each provider.constyProvider=newYjsProvider(/* ... */)yProvider.on('sync', setConnected)setSharedType(sharedDoc)setProvider(yProvider)return () => {yDoc?.destroy()yProvider?.off('sync', setConnected)yProvider?.destroy() } }, [])if (!connected ||!sharedType ||!provider) {return <div>Loading…</div> }return <SlateEditorsharedType={sharedType} provider={provider} />}constSlateEditor= ({ sharedType, provider }) => {consteditor=useMemo(() => {conste=withReact(withYjs(createEditor(), sharedType))// Ensure editor always has at least 1 valid childconst { normalizeNode } = ee.normalizeNode= entry => {const [node] = entryif (!Editor.isEditor(node) ||node.children.length>0) {returnnormalizeNode(entry) }Transforms.insertNodes(editor, initialValue, { at: [0] }) }return e }, [])useEffect(() => {YjsEditor.connect(editor)return () =>YjsEditor.disconnect(editor) }, [editor])return ( <Slateeditor={editor} initialValue={initialValue}> <Editable /> </Slate> )}
That's all you need to attach Yjs to Slate!
Let's look at a real-world example of setting up Yjs—here's a code snippet for setting up a Liveblocks provider. Liveblocks uses the concept of rooms, spaces where users can collaborative. To use a Liveblocks provider, you join a multiplayer room with RoomProvider, then pass the room to new LiveblocksProvider, along with the Yjs document.
import LiveblocksProvider from'@liveblocks/yjs'import { RoomProvider, useRoom } from'../liveblocks.config'// Join a Liveblocks room and show the editor after connectingexportconstApp= () => {return ( <RoomProviderid="my-room-name"initialPresence={{}}> <ClientSideSuspensefallback={<div>Loading…</div>}> {() => <CollaborativeEditor />} </ClientSideSuspense> </RoomProvider> )}exportconstCollaborativeEditor= () => {constroom=useRoom()const [connected,setConnected] =useState(false)const [sharedType,setSharedType] =useState()const [provider,setProvider] =useState()// Connect to your Yjs provider and documentuseEffect(() => {constyDoc=newY.Doc()constsharedDoc=yDoc.get('slate',Y.XmlText)// Set up your Liveblocks provider with the current room and documentconstyProvider=newLiveblocksProvider(room, yDoc)yProvider.on('sync', setConnected)setSharedType(sharedDoc)setProvider(yProvider)return () => {yDoc?.destroy()yProvider?.off('sync', setConnected)yProvider?.destroy() } }, [room])if (!connected ||!sharedType ||!provider) {return <div>Loading…</div> }return <SlateEditorsharedType={sharedType} provider={provider} />}constSlateEditor= ({ sharedType, provider }) => {// ...}
Unlike other providers, Liveblocks hosts your Yjs back end for you, which means you don't need to run your own server to get this working. For more information on setting up Liveblocks providers, make sure to read their Slate getting started guide.
Note that Liveblocks is independent of the Slate project, and isn't required for collaboration, but it may be convenient depending on your needs. Other providers are available should you wish to set up and host a Yjs back end yourself.
After setting up Yjs, it's possible to add multiplayer cursors to your app. You can do this with hooks supplied by slate-yjs, which allow you to find the cursor positions of other users. Here's an example of setting up a cursor component.
You can then import this into your SlateEditor component. Notice that we're using withCursors from slate-yjs, adding provider.awareness and the current user's name to it. We're then wrapping <Editable> in the new <Cursors> component we've just created.