Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.velt.dev/llms.txt

Use this file to discover all available pages before exploring further.

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

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>
  );
}

Step 2: Read the store

Read the current array value at any time. The CRDT library keeps the value automatically synced with all collaborators.
The hook’s value field reactively tracks the current array — re-renders automatically on every local and remote change, so no manual subscription is needed. You can also read the latest synchronous value at any time via store.getValue() (useful inside event handlers).
const { value: items, store } = useStore<Item[]>({
  storeId: 'my-array-store',
  type: 'array',
  initialValue: [],
});

// Reactive value from the hook — already up to date
const itemList = Array.isArray(items) ? items : [];

// Synchronous read of the latest value (useful inside event handlers)
const current = store.getValue() || [];

Step 3: Update the store

Use update() to replace the entire array value. 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));
  }
};

Step 4: Save and restore versions (optional)

Create checkpoints and roll back when needed.
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);
}

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.
const { value: items, update: updateItems } = useStore<Item[]>({
  storeId: 'my-array-store',
  type: 'array',
  initialValue: defaultItems,
  forceResetInitialContent: true,
});

Complete Example

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>
  );
};