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

shell
npm install @inrealtime/react

Yarn

shell
yarn 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.
typescript
import { 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.
typescript
import { 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.
typescript
import { 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.
typescript
import { 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.
typescript
import { 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.
typescript
import { 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.
typescript
import { 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.
typescript
export 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.
typescript
import { 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.
typescript
import { 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.
typescript
import { 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.
typescript
import { 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.
typescript
import { 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.
typescript
import { 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.
typescript
import { 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:
typescript
broadcast('emojiSent', { data: { emoji: '🔥' }, recipientClientIds: [ ... ] })

useBroadcastListener

Listens to broadcasted events.
typescript
import { 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
typescript
import { 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
typescript
import { 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
  • 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.
typescript
import { 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.
typescript
import { 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.
typescript
import { 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.
typescript
import { 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