Realtime offers a simple and intuitive way to integrate realtime functionality into your app, allowing multiple users to collaborate in real time. This usage guide will take you through the installation process, as well as show you how to use Realtime hooks and functionality to enhance your app's collaboration capabilities. Whether you're a seasoned programmer or new to the world of React, Realtime makes it easy to add realtime collaboration to your app.
While we highly recommend using TypeScript due to its benefits such as better code maintainability and fewer runtime errors, we understand that not everyone is comfortable with it. In such cases, using JavaScript is perfectly fine.
Tips
Realtime query hooks use Zustand selectors, if you are not familiar with them you might want to check it out here on Zustand Documentation. Also, it might be worth it to check out Immer performance tips and Immer Pitfalls since the
patch uses Immer to mutate data.Installation
When you have successfully created a project (see here) you can start developing using Realtime. Now, let’s start by installing the Realtime React package into the project at hand.
NPM
shellnpm install @inrealtime/react
Yarn
shellyarn add @inrealtime/react
Setup
Realtime also offers simple ways of connecting to multiple documents at the same time, using a single connection. It also offers way of connecting to differently typed documents and more. See Advanced section below (groups).
createRealtimeContext
For most cases, we recommend using the RealtimeProvider method for providing the application with access to Realtime data. The provider is exported from a function called
createRealtimeContext which also exports all the hooks we will need to get and set our data. In the example below we are creating the file realtime.config.ts that will export all our hooks as well as the context provider we will need for our setup.typescriptimport { createRealtimeContext } from '@inrealtime/react' type Document = { title: string items: { id: string title: string isCompleted?: boolean }[] } type Presence = { id: string name: string cursor?: { x: number; y: number } } export const { RealtimeProvider, useRealtimeContext, useConnectionStatus, usePresenceStatus, useDocumentStatus, useStore, usePatch, useSubscribe, useCollaborators, useSubscribeCollaborators, useMe, usePatchMe, useSubscribeMe, useBroadcast, useBroadcastListener, } = createRealtimeContext<Document, Presence>()
RealtimeProvider
We can import the
RealtimeProvider from the config file and wrap a part of the app we would like to have connected with Realtime, in this case the whole app. All components rendered inside this provider will now be able to use the hooks exported from the config file.typescriptimport { RealtimeProvider } from 'realtime.config' const getAuthToken = useCallback(async () => { const accessTokenResponse = await getAuthTokenFromMyAPI() return (await accessTokenResponse.json()).token }, []) const App = () => { return ( <RealtimeProvider documentId={'myDocument'} // This can be any string, if the document does not exist in the user's database, it will be created. throttle={100} // This is optional, default is 50ms, this determines the throttle for all our data communication. publicAuthKey={process.env.REALTIME_PUBLIC_AUTH_KEY} // Optional, if you don't want to use custom authorization. getAuthToken={getAuthToken} // Custom fetch function for authentication using the secret key. Not necessary if using the publicAuthKey prop. > {/* App content */} </RealtimeProvider> ) }
useConnectionStatus
Returns the status of the Realtime connection. The status is of type
RealtimeConnectionStatus and can have one of the following values: Closed, Connecting, Authenticating, Open.typescriptimport { useConnectionStatus } from 'realtime.config' const connectionStatus = useConnectionStatus()
usePresenceStatus
Returns the status of the presence and whether you can start using
useCollaborators and patchMe. The status is of type RealtimePresenceStatus and can have one of the following values: Unready, Ready.typescriptimport { usePresenceStatus } from 'realtime.config' const presenceStatus = usePresenceStatus()
useDocumentStatus
Returns the status of the Realtime document, and whether you can start using
useStore and patch. The status is of type RealtimeDocumentStatus and can have one of the following values: Unready, Ready.typescriptimport { useDocumentStatus } from 'realtime.config' const documentStatus = useDocumentStatus()
Data
useStore
The useStore hook extends Zustand’s useStore hook for getting data from the Realtime state and fully supports all of its features, including selectors and comparison functions. We recommend checking out their documentation for further information regarding selectors and comparison functions.
typescriptimport { useStore } from 'realtime.config' const title = useStore((root) => root.title) const incompletedItems = useStore((root) => root.items?.filter((item) => !item.isCompleted))
usePatch
The patch function returned from the usePatch hook uses the power of immer so you can simply patch any data as using regular javascript.
It might be worth it to check out Immer performance tips and Immer Pitfalls.
typescriptimport { usePatch } from 'realtime.config' const patch = usePatch() const addItem = () => { patch((root) => { root.items.push({ id: uuidv4(), title: workingTitle, isComplete: false, }) }) } const removeItem = () => { patch((root) => { const found = findProperty(root) if (!found) { return } const { property: foundProperty, parent } = found const propertyIndex = parent.properties.indexOf(foundProperty) parent.properties.splice(propertyIndex, 1) }) } const onChange = useCallback( (e: ChangeEvent<HTMLInputElement>) => { patch((root) => { const nodeData = root.metadata.http.nodes.find((n) => n.id === nodeId).data if (nodeData) { nodeData.path = e.target.value } }) }, [patch, nodeId], )
Presence
Clients
Clients are active connected users. They are accessible through
collaborators and me hooks.typescriptexport type PresenceClient<TRealtimePresenceData> = { clientId: string metadata: { [key: string]: any userId: string permissions: Permissions } data: TRealtimePresenceData dataUpdatedAt: string }
- The metadata contains info from the authentication process, and can include things like names. See more at here.
- The permissions show what permission a user has within the connected document and can for example be useful if implementing a read-only view of a document. Permission types can be seen here.
useCollaborators
Like the useStore hook, useCollaborators uses the same functionality as Zustand’s useStore hook. It returns the presence data of all other users (not yourself) that are connected to the same document as you.
typescriptimport { useCollaborators } from 'realtime.config' export const Cursors = () => { const collaborators = useCollaborators() // Can also use a selector here return ( <div> {collaborators ?.filter((collaborator) => !!collaborator.data?.cursor) ?.map((collaborator) => { return ( <Cursor key={collaborator.clientId} x={collaborator.data.cursor?.x} y={collaborator.data.cursor?.y} color={collaborator.metadata?.color} name={collaborator.metadata?.name} /> ) })} </div> ) }
useMe
Returns your presence data, uses Zustand’s selector functionality like all our query hooks.
typescriptimport { useMe } from 'realtime.config' const myName = useMe((me) => me.metadata?.name)
usePatchMe
Returns a function that can be used to mutate your presence data. By default the values inside
patchMe are appended to the current data (and overwrite the same keys). If replace flag is set to true the whole data object is replaced for the current user. typescriptimport { usePatchMe } from 'realtime.config' const patchMe = usePatchMe() const changeColor = (color: string) => { patchMe({ color }) } const resetMyPresence = () => { patchMe({}, { replace: true }) }
Subscriptions
Subscriptions are a way for you to subscribe to changes to specific data in your application. You can hook into the before and after changes of the data and perform some actions based on those changes. Subscriptions are useful for implementing real-time updates, triggering events, and more.
Subscription hooks below use Zustand subscribe with selector functionality, so its worth glimpsing at. Also worth noting is this.
useSubscribe
Used to subscribe to the realtime document. Takes in a selector that can be used to define which part of the data structure it should subscribe to.
typescriptimport { useSubscribe } from 'realtime.config' import { shallow } from 'zustand/shallow' const subscribe = useSubscribe() useEffect(() => { return subscribe((root) => root.todos, console.log, { equalityFn: shallow }) // shallow from Zustand (not required) }, [subscribe])
useSubscribeCollaborators
Same as useSubscribe, but for collaborators data.
typescriptimport { useSubscribeCollaborators } from 'realtime.config' import { shallow } from 'zustand/shallow' const subscribeCollaborators = useSubscribeCollaborators() useEffect(() => { return subscribeCollaborators((collaborators) => collaborators[0].metadata, console.log, { equalityFn: shallow }) // shallow from Zustand (not required) }, [subscribe])
useSubscribeMe
Same as useSubscribe, but for presence data.
typescriptimport { useSubscribeMe } from 'realtime.config' const subscribeMe = useSubscribeMe() useEffect(() => { return subscribeMe((me) => me.data?.cursor, console.log, { equalityFn: shallow }) // shallow from Zustand }, [subscribe])
Broadcasting
useBroadcast
Used to broadcast realtime data to all broadcast listeners.
typescriptimport { broadcast } from 'realtime.config' const broadcast = useBroadcast() broadcast('emojiSent', { data: { emoji: '🔥' } })
You can also send to specific connected clients (client id’s can be retrieved presence hooks, see documentation above) by doing the following:
typescriptbroadcast('emojiSent', { data: { emoji: '🔥' }, recipientClientIds: [ ... ] })
useBroadcastListener
Listens to broadcasted events.
typescriptimport { useBroadcastListener } from 'realtime.config' useBroadcastListener((event) => { if (event.type === 'emojiSent') { displayEmoji(event.data.moji) } })
Advanced
Groups - Single connection with multiple documents
Groups are a way to connect to multiple documents in a single connection. By grouping related documents together, you can streamline your collaboration process and more efficiently manage your documents. This is particularly helpful for projects that involve a large number of documents or where users need to switch between different documents frequently.
Here are some benefits of using groups:
- Faster subscribing: With Group IDs, you can subscribe to multiple documents in a single connection, which can improve performance and reduce latency.
- Easier management: Group IDs can be used to organize documents into logical groups, which makes it easier to manage and maintain large sets of documents.
- More efficient resource utilization: By subscribing to multiple documents in a single connection, you can reduce the resources required for maintaining multiple connections.
- Makes it easier to implement Realtime collaboration features in complex systems. By connecting related documents with a Group ID, users can collaborate on multiple documents simultaneously, without the need to switch between different connections or applications. This can lead to more efficient and effective collaboration, which can improve the overall performance of the system.
createRealtimeGroupContext & createRealtimeDocumentContext
typescriptimport { createRealtimeDocumentContext, createRealtimeGroupContext } from '@inrealtime/react' type Presence = { id: string name: string cursor?: { x: number; y: number } } export const { RealtimeGroupProvider, useRealtimeGroupContext, useConnectionStatus, usePresenceStatus, useCollaborators, useSubscribeCollaborators, useMe, usePatchMe, useSubscribeMe, useBroadcast, useBroadcastListener, } = createRealtimeGroupContext<Presence>() type Document = { title: string items: { id: string title: string isCompleted?: boolean }[] } // Note: We can create multiple of these below with different types if we wish export const { RealtimeDocumentProvider, useRealtimeDocumentContext, useDocumentStatus, useStore, usePatch, useSubscribe, } = createRealtimeDocumentContext<Document>({ useRealtimeGroupContext })
RealtimeGroupProvider & RealtimeDocumentProvider
typescriptimport { RealtimeDocumentProvider, RealtimeGroupProvider } from 'realtime.config' const getAuthToken = useCallback(async () => { const accessTokenResponse = await getAuthTokenFromMyAPI() return (await accessTokenResponse.json()).token }, []) const App = () => { return ( <RealtimeGroupProvider groupId={'myGroupId'} // The group id groups together the below documents into a single connection throttle={100} // This is optional, default is 50ms, this determines the throttle for all our data communication. publicAuthKey={process.env.REALTIME_PUBLIC_AUTH_KEY} // Optional, if you don't want to use custom authorization. getAuthToken={getAuthToken} // Custom fetch function for authentication using the secret key. Not necessary if using the publicAuthKey prop. > <RealtimeDocumentProvider documentId={'myDocumentId1'} // If using getAuthToken, then this must follow the document ID prefix > {/* App content for this document */} </RealtimeDocumentProvider> <RealtimeDocumentProvider documentId={'myDocumentId2'} // If using getAuthToken, then this must follow the document ID prefix > {/* App content for this document */} </RealtimeDocumentProvider> </RealtimeGroupProvider> ) }
Notes
- If using
getAuthTokenthen you must usegroupIdin Authentication
- Groups should not have overlapping document ids, i.e. no two groups should have the same document ids as it may result in overriding of documents.
- Information regarding group id’s can be seen in Authentication
- Group IDs can be a maximum of 96 characters long
- Please note that documents in groups and single document connections are kept separate. Using the same document ID for different groups will result in distinct documents. Similarly, connecting to a document ID with a single document connection and connecting to a group with the same document ID will not yield the same document.
useRealtime
If for some reason you don’t wish to use the RealtimeProvider for providing your application with the Realtime state, you can use this hook instead. This might be useful if you want to create your own context provider. It takes in the same parameters as the RealtimeProvider component and returns all the hooks you will need to access the Realtime data store.
typescriptimport { useRealtime } from '@inrealtime/react' const { connectionStatus, presenceStatus, documentStatus, useStore, patch, subscribe, useCollaborators, subscribeCollaborators, useMe, patchMe, subscribeMe, broadcast, useBroadcastListener, } = useRealtime<Document, Presence>({ documentId, throttle, publicAuthKey, getAuthToken, })
useRealtimeContext
Can be used to get access to the context created with
createRealtimeContext.typescriptimport { useRealtimeContext } from 'realtime.config' const { connectionStatus, presenceStatus, documentStatus, useStore, patch, subscribe, useCollaborators, subscribeCollaborators, useMe, patchMe, subscribeMe, broadcast, useBroadcastListener, } = useRealtimeContext()
useRealtimeGroupContext
Can be used to get access to the context created with
createRealtimeGroupContext.typescriptimport { useRealtimeGroupContext } from 'realtime.config' const { connectionStatus, presenceStatus, useCollaborators, subscribeCollaborators, useMe, patchMe, subscribeMe, broadcast, useBroadcastListener, } = useRealtimeGroupContext()
useRealtimeDocumentContext
Can be used to get access to the context created with
createRealtimeDocumentContext.typescriptimport { useRealtimeDocumentContext } from 'realtime.config' const { documentStatus, useStore, patch, subscribe, } = useRealtimeDocumentContext()
Features in Beta
It's important to understand that features in beta are not ready for production, may not be stable and could potentially cause errors or other issues. Beta features may also be changed or removed in future releases.
Autosave & long term offline
Our team is currently working on an new feature for our software package, an
autosave functionality that allows users to work offline for extended periods while all changes made are stored locally and synced when reconnected. This feature makes it more convenient for users to work without a stable internet connection, or without an internet connection at all. When a connection is reestablished, the changes are synced automatically, ensuring that the document is up to date and available.As part of our efforts to enhance the Realtime package interface, we are exploring new ways to make the syncing process more efficient. This includes the ability to selectively sync messages, view the number of changes, apply changes, and more. Our current approach involves storing all changes made to the document in IndexedDB, which allows for quick and easy syncing when a connection is reestablished. However, environments that do not support IndexedDB will not be able to utilize this feature. All modern browsers support IndexedDB
We welcome any feedback or suggestions on how we can improve this feature or expand its availability to more environments. If you have any thoughts or ideas, please don't hesitate to contact us at support@inrealtime.app. Our team is dedicated to delivering the best possible user experience for our software package.
It's important to note that while the 'autosave' feature is not yet ready for production, it can be enabled by setting the autosave flag on the
RealtimeProvider, RealtimeDocumentProvider or useRealtime hook. typescript<RealtimeProvider ... autosave={true} > {/* App content */} </RealtimeProvider>
Coming soon
useUndo
Coming soon
useRedo
Coming soon