Complete Steps 1-2 on the Core setup page before continuing. Those steps install dependencies and initialize Velt.
Setup
Step 1: Create a CRDT array store
- React / Next.js
- Other Frameworks
Use the
useStore hook to create a CRDT store backed by a Yjs Y.Array. The hook handles store initialization, React lifecycle, and real-time subscriptions automatically — no manual useState/useEffect needed for store setup.import { useStore } from '@veltdev/crdt-react';
interface Item {
id: string;
name: string;
}
function Component() {
const {
value: items,
update: updateItems,
store,
isLoading,
isSynced,
status,
error,
} = useStore<Item[]>({
storeId: 'my-array-store',
type: 'array',
initialValue: [{ id: '1', name: 'First item' }],
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
const itemList = Array.isArray(items) ? items : [];
return (
<ul>
{itemList.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
Use
createVeltStore to create a CRDT store backed by a Yjs Y.Array. The initialValue is only applied when the document is brand-new (no prior remote state exists).import { createVeltStore } from '@veltdev/crdt';
async function initializeStore(client) {
const store = await createVeltStore({
id: 'my-array-store',
type: 'array',
initialValue: [{ id: '1', name: 'First item' }],
veltClient: client,
});
if (!store) return;
// Seed the UI with the current CRDT value
const items = store.getValue() || [];
renderItems(items);
}
Step 2: Read and update the store
- React / Next.js
- Other Frameworks
The hook’s
value field reactively tracks the current array — no manual subscription needed. Use update() to replace the entire array value, or read the latest value from store.getValue() before applying mutations. The CRDT library handles conflict-free merging with other users’ edits automatically.// Add a new item to the end of the array
const addItem = (name: string) => {
const current = store.getValue() || [];
if (Array.isArray(current)) {
updateItems([...current, { id: generateId(), name }]);
}
};
// Update an item by its ID
const updateItem = (itemId: string, newName: string) => {
const current = store.getValue() || [];
if (Array.isArray(current)) {
updateItems(
current.map((item) => (item.id === itemId ? { ...item, name: newName } : item))
);
}
};
// Remove an item from the array by its ID
const removeItem = (itemId: string) => {
const current = store.getValue() || [];
if (Array.isArray(current)) {
updateItems(current.filter((item) => item.id !== itemId));
}
};
Use
store.getValue() to read the current value and store.update() to replace the entire value. The CRDT library handles conflict-free merging with other users’ edits automatically.// Read the current array value
const currentItems = store.getValue();
// Add a new item to the end of the array
function addItem(name) {
const current = store.getValue() || [];
store.update([...current, { id: generateId(), name }]);
}
// Update an item by its ID
function updateItem(itemId, newName) {
const current = store.getValue() || [];
store.update(
current.map((item) => (item.id === itemId ? { ...item, name: newName } : item))
);
}
// Remove an item from the array by its ID
function removeItem(itemId) {
const current = store.getValue() || [];
store.update(current.filter((item) => item.id !== itemId));
}
Step 3: Subscribe to real-time changes (Other Frameworks)
Usestore.subscribe() to listen for changes from all collaborators. The callback fires on every change (local and remote) with the full merged array.
// Subscribe to all future changes (local and remote)
const unsubscribe = store.subscribe((newItems) => {
// Re-render the UI with the latest merged state
renderItems(Array.isArray(newItems) ? newItems : []);
});
// Call unsubscribe to stop listening when no longer needed
unsubscribe();
In React, the
value from useStore is already reactive — no manual subscription is needed.Step 4: Save and restore versions (optional)
Create checkpoints and roll back when needed.- React / Next.js
- Other Frameworks
The
useStore hook exposes version management methods directly:import { Version } from '@veltdev/crdt-react';
const {
value: items,
update: updateItems,
saveVersion,
getVersions,
getVersionById,
restoreVersion,
setStateFromVersion,
} = useStore<Item[]>({
storeId: 'my-array-store',
type: 'array',
initialValue: [],
});
// Save a named snapshot of the current state
await saveVersion('Draft v1');
// Retrieve the list of all saved versions
const versions: Version[] = await getVersions();
// Restore the store to a previously saved version
await restoreVersion(versionId);
const version = await getVersionById(versionId);
if (version) {
await setStateFromVersion(version);
}
import { Version } from '@veltdev/crdt';
// Save a named snapshot of the current state
const versionId = await store.saveVersion('Draft v1');
// List all saved versions
const versions = await store.getVersions();
// Restore the store to a previously saved version
await store.restoreVersion(versionId);
const version = await store.getVersionById(versionId);
if (version) {
await store.setStateFromVersion(version);
}
Step 5: Initial content with forceResetInitialContent (optional)
By default,initialValue is only applied when the document has no existing remote state. Set forceResetInitialContent to true to always reset the store to initialValue on initialization, overwriting any existing remote data.
- React / Next.js
- Other Frameworks
const { value: items, update: updateItems } = useStore<Item[]>({
storeId: 'my-array-store',
type: 'array',
initialValue: defaultItems,
forceResetInitialContent: true,
});
const store = await createVeltStore({
id: 'my-array-store',
type: 'array',
initialValue: defaultItems,
veltClient: client,
forceResetInitialContent: true,
});
Complete Example
- React / Next.js
- Other Frameworks
A complete collaborative todo list built with
useStore:Complete Implementation
import React, { useState, useEffect, useCallback } from 'react';
import { useStore, Version } from '@veltdev/crdt-react';
interface Todo {
id: string;
text: string;
completed: boolean;
}
const generateId = () => Math.random().toString(36).substring(2, 15);
const defaultTodos: Todo[] = [
{ id: 'seed-1', text: 'Welcome Todo - Edit me!', completed: false },
];
export const Todos = () => {
const [newTodo, setNewTodo] = useState('');
const [versionName, setVersionName] = useState('');
const [versions, setVersions] = useState<Version[]>([]);
// Use the useStore hook — handles initialization and subscriptions automatically
const {
value: todos,
update: updateTodos,
store,
saveVersion: storeSaveVersion,
getVersions: storeGetVersions,
restoreVersion: storeRestoreVersion,
getVersionById,
setStateFromVersion,
} = useStore<Todo[]>({
storeId: 'my-todo-store',
type: 'array',
initialValue: defaultTodos,
});
// Fetch saved versions when store is ready
const refreshVersions = useCallback(async () => {
const v = await storeGetVersions();
setVersions(v);
}, [storeGetVersions]);
useEffect(() => {
if (store) refreshVersions();
}, [refreshVersions, store]);
const todoList = Array.isArray(todos) ? todos : [];
// Add a new todo
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (newTodo.trim() && store) {
const newItem: Todo = { id: generateId(), text: newTodo.trim(), completed: false };
const current = store.getValue() || [];
if (Array.isArray(current)) {
updateTodos([...current, newItem]);
}
setNewTodo('');
}
};
// Toggle a todo's completed state
const toggleTodo = (todoId: string) => {
if (!store) return;
const current = store.getValue();
if (Array.isArray(current)) {
updateTodos(current.map((t: Todo) => t.id === todoId ? { ...t, completed: !t.completed } : t));
}
};
// Delete a todo
const deleteTodo = (todoId: string) => {
if (!store) return;
const current = store.getValue();
if (Array.isArray(current)) {
updateTodos(current.filter((t: Todo) => t.id !== todoId));
}
};
// Handle saving a new version
const handleSaveVersion = async (e: React.FormEvent) => {
e.preventDefault();
if (versionName.trim()) {
await storeSaveVersion(versionName.trim());
setVersionName('');
await refreshVersions();
}
};
// Handle restoring a version
const handleRestoreVersion = async (versionId: string) => {
await storeRestoreVersion(versionId);
const version = await getVersionById(versionId);
if (version) {
await setStateFromVersion(version);
}
await refreshVersions();
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add a new todo..."
/>
<button type="submit">Add</button>
</form>
<ul>
{todoList.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
<h3>Versions</h3>
<form onSubmit={handleSaveVersion}>
<input
type="text"
value={versionName}
onChange={(e) => setVersionName(e.target.value)}
placeholder="Version name..."
/>
<button type="submit">Save Version</button>
</form>
<ul>
{versions.map((version) => (
<li key={version.versionId}>
<span>{version.versionName}</span>
<button onClick={() => handleRestoreVersion(version.versionId)}>Restore</button>
</li>
))}
</ul>
</div>
);
};
A complete collaborative todo list with SDK initialization, store management, and version control.velt.tstodos.tsmain.tsHTML structure
Complete velt.ts
import { initVelt } from '@veltdev/client';
import type { Velt } from '@veltdev/types';
let client: Velt | null = null;
let veltInitialized = false;
// Subscriber registry for SDK-ready notifications
const veltInitSubscribers = new Map<string, (velt: Velt) => void>();
async function initializeVelt() {
// Initialize the Velt SDK
client = await initVelt('YOUR_API_KEY');
// Scope all collaboration to the configured document
client.setDocument('crdt-array-demo-doc-1', { documentName: 'CRDT Array Demo' });
// Track user login/logout
client.getCurrentUser().subscribe((currentUser) => {
renderUserControls(currentUser);
});
// Track SDK-ready state and notify subscribers
client.getVeltInitState().subscribe((isReady) => {
veltInitialized = isReady;
if (isReady && client) {
veltInitSubscribers.forEach((callback) => callback(client));
}
});
}
// Subscribe to the SDK being fully initialized and ready
export const subscribeToVeltInit = (subscriberId: string, callback: (velt: Velt) => void) => {
veltInitSubscribers.set(subscriberId, callback);
if (veltInitialized && client) {
callback(client);
}
};
// Authenticate a user with the Velt backend
export async function loginWithUser(userId: string) {
await client?.identify({ userId, name: userId });
}
// Sign the current user out
export async function logout() {
await client?.signOutUser();
}
// Start SDK initialization on module load
initializeVelt();
Complete todos.ts
import { Store, Version, createVeltStore } from '@veltdev/crdt';
import type { Velt } from '@veltdev/types';
import { subscribeToVeltInit } from './velt';
interface Todo {
id: string;
text: string;
completed: boolean;
}
let store: Store<Todo[]> | null = null;
let todos: Todo[] = [];
let versions: Version[] = [];
// Initialize the store when the SDK is ready
async function initStore(veltClient: Velt) {
// Create the CRDT array store with initial seed data
const todoStore = await createVeltStore<Todo[]>({
id: 'my-todo-store',
type: 'array',
initialValue: [{ id: 'seed-1', text: 'Welcome Todo - Edit me!', completed: false }],
veltClient: veltClient,
});
if (!todoStore) return;
store = todoStore;
// Seed UI with the current CRDT value
todos = todoStore.getValue() || [];
renderTodos();
// Subscribe to all future changes (local and remote)
todoStore.subscribe((newTodos) => {
todos = Array.isArray(newTodos) ? newTodos : [];
renderTodos();
});
// Load saved versions
await refreshVersions();
}
// Wait for the SDK to be ready, then initialize the store
subscribeToVeltInit('todos', (velt) => {
initStore(velt);
});
// Add a new todo to the end of the list
function addTodo(text: string) {
if (!store) return;
const current = store.getValue() || [];
const newTodo: Todo = { id: Math.random().toString(36).substring(2, 15), text, completed: false };
store.update([...current, newTodo]);
}
// Toggle the completed state of a todo by its ID
function toggleTodo(todoId: string) {
if (!store) return;
const current = store.getValue() || [];
store.update(
current.map((todo) => (todo.id === todoId ? { ...todo, completed: !todo.completed } : todo))
);
}
// Remove a todo from the list by its ID
function deleteTodo(todoId: string) {
if (!store) return;
const current = store.getValue() || [];
store.update(current.filter((todo) => todo.id !== todoId));
}
// Save a named snapshot of the current state
async function saveVersionHandler(name: string) {
if (!store) return;
await store.saveVersion(name);
await refreshVersions();
}
// Restore the store to a previously saved version
async function restoreVersionHandler(versionId: string) {
if (!store) return;
await store.restoreVersion(versionId);
const version = await store.getVersionById(versionId);
if (version) {
await store.setStateFromVersion(version);
}
await refreshVersions();
}
// Fetch the latest version list from the backend
async function refreshVersions() {
if (!store) return;
versions = await store.getVersions();
renderVersions();
}
// Render the todo list into the DOM
function renderTodos() {
const todoList = document.querySelector('.todo-list');
if (!todoList) return;
todoList.innerHTML = '';
for (const todo of todos) {
const li = document.createElement('li');
li.className = `todo-item${todo.completed ? ' completed' : ''}`;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = todo.completed;
checkbox.addEventListener('change', () => toggleTodo(todo.id));
const span = document.createElement('span');
span.textContent = todo.text;
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.addEventListener('click', () => deleteTodo(todo.id));
li.appendChild(checkbox);
li.appendChild(span);
li.appendChild(deleteBtn);
todoList.appendChild(li);
}
}
// Render the version list into the DOM
function renderVersions() {
const versionList = document.querySelector('.version-list');
if (!versionList) return;
versionList.innerHTML = '';
for (const version of versions) {
const li = document.createElement('li');
const nameSpan = document.createElement('span');
nameSpan.textContent = version.versionName;
const restoreBtn = document.createElement('button');
restoreBtn.textContent = 'Restore';
restoreBtn.addEventListener('click', () => restoreVersionHandler(version.versionId));
li.appendChild(nameSpan);
li.appendChild(restoreBtn);
versionList.appendChild(li);
}
}
// Attach form submit handlers to the DOM
export function setupTodoForm() {
const todoForm = document.getElementById('todo-form');
if (todoForm) {
todoForm.addEventListener('submit', (event) => {
event.preventDefault();
const input = todoForm.querySelector('.todo-input') as HTMLInputElement;
if (input && input.value.trim()) {
addTodo(input.value.trim());
input.value = '';
}
});
}
const versionForm = document.getElementById('version-form');
if (versionForm) {
versionForm.addEventListener('submit', (event) => {
event.preventDefault();
const input = versionForm.querySelector('.version-input') as HTMLInputElement;
if (input && input.value.trim()) {
saveVersionHandler(input.value.trim());
input.value = '';
}
});
}
}
Complete main.ts
import './style.css';
import './velt';
import { setupTodoForm } from './todos';
// Attach form handlers once the DOM is ready
setupTodoForm();
Complete index.html
<div class="app-container">
<header class="app-header">
<h1>CRDT Array Demo</h1>
<div id="user-controls"></div>
</header>
<main class="app-content">
<div id="todos-header">Collaborative Todos - Please login to start editing</div>
<form id="todo-form">
<input type="text" class="todo-input" placeholder="Add a new todo..." />
<button type="submit">Add</button>
</form>
<ul class="todo-list"></ul>
<div class="versions-section">
<h3>Versions</h3>
<form id="version-form">
<input type="text" class="version-input" placeholder="Version name..." />
<button type="submit">Save Version</button>
</form>
<ul class="version-list"></ul>
</div>
</main>
</div>

