Skip to main content
Complete Steps 1-2 on the Core setup page before continuing. Those steps install dependencies and initialize Velt.

Setup

Step 1: Create a CRDT map store

Use the useStore hook with type: 'map' to create a CRDT store backed by a Yjs Y.Map. 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';

type DataMap = Record<string, any>;

function Component() {
  const {
    value: entries,
    update: updateEntries,
    store,
    isLoading,
    isSynced,
    status,
    error,
  } = useStore<DataMap>({
    storeId: 'my-map-store',
    type: 'map',
    initialValue: { key1: 'value1', key2: 'value2' },
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  const entriesMap = (entries && typeof entries === 'object' && !Array.isArray(entries)) ? entries : {};

  return (
    <ul>
      {Object.entries(entriesMap).map(([key, value]) => (
        <li key={key}>{key}: {value}</li>
      ))}
    </ul>
  );
}

Step 2: Read and update the store

The hook’s value field reactively tracks the current map — no manual subscription needed. Use update() to replace the entire map 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 or overwrite a key-value pair
const addEntry = (key: string, value: any) => {
  const current = store.getValue() || {};
  updateEntries({ ...current, [key]: value });
};

// Update the value for an existing key
const updateEntry = (key: string, value: any) => {
  const current = store.getValue() || {};
  updateEntries({ ...current, [key]: value });
};

// Remove a key-value pair from the map
const deleteEntry = (key: string) => {
  const current = store.getValue() || {};
  const updated = { ...current };
  delete updated[key];
  updateEntries(updated);
};

Step 3: Subscribe to real-time changes (Other Frameworks)

Use store.subscribe() to listen for changes from all collaborators. The callback fires on every change (local and remote) with the full merged map.
// Subscribe to all future changes (local and remote)
const unsubscribe = store.subscribe((newData) => {
  // Re-render the UI with the latest merged state
  renderEntries(newData && typeof newData === 'object' ? newData : {});
});

// 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.
The useStore hook exposes version management methods directly:
import { Version } from '@veltdev/crdt-react';

const {
  value: entries,
  update: updateEntries,
  saveVersion,
  getVersions,
  getVersionById,
  restoreVersion,
  setStateFromVersion,
} = useStore<DataMap>({
  storeId: 'my-map-store',
  type: 'map',
  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: entries, update: updateEntries } = useStore<DataMap>({
  storeId: 'my-map-store',
  type: 'map',
  initialValue: defaultEntries,
  forceResetInitialContent: true,
});

Complete Example

A complete collaborative key-value editor built with useStore:
Complete Implementation
import React, { useState, useEffect, useCallback } from 'react';
import { useStore, Version } from '@veltdev/crdt-react';

type KVMap = Record<string, string>;

const defaultEntries: KVMap = {
  greeting: 'Hello World',
  language: 'TypeScript',
  framework: 'React',
};

export const KeyValueStore = () => {
  const [newKey, setNewKey] = useState('');
  const [newValue, setNewValue] = useState('');
  const [versionName, setVersionName] = useState('');
  const [versions, setVersions] = useState<Version[]>([]);

  // Use the useStore hook — handles initialization and subscriptions automatically
  const {
    value: entries,
    update: updateEntries,
    store,
    saveVersion: storeSaveVersion,
    getVersions: storeGetVersions,
    restoreVersion: storeRestoreVersion,
    getVersionById,
    setStateFromVersion,
  } = useStore<KVMap>({
    storeId: 'my-kvstore',
    type: 'map',
    initialValue: defaultEntries,
  });

  // 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 entriesMap = (entries && typeof entries === 'object' && !Array.isArray(entries)) ? entries : {};

  // Add a new entry
  const handleAddEntry = (e: React.FormEvent) => {
    e.preventDefault();
    if (newKey.trim() && newValue.trim() && store) {
      const current = store.getValue() || {};
      updateEntries({ ...current, [newKey.trim()]: newValue.trim() });
      setNewKey('');
      setNewValue('');
    }
  };

  // Update an existing entry
  const handleUpdateEntry = (key: string, value: string) => {
    if (!store) return;
    const current = store.getValue() || {};
    updateEntries({ ...current, [key]: value });
  };

  // Delete an entry
  const handleDeleteEntry = (key: string) => {
    if (!store) return;
    const current = store.getValue() || {};
    const updated = { ...current };
    delete updated[key];
    updateEntries(updated);
  };

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

  const entryKeys = Object.keys(entriesMap);

  return (
    <div>
      <form onSubmit={handleAddEntry}>
        <input
          type="text"
          value={newKey}
          onChange={(e) => setNewKey(e.target.value)}
          placeholder="Key..."
        />
        <input
          type="text"
          value={newValue}
          onChange={(e) => setNewValue(e.target.value)}
          placeholder="Value..."
        />
        <button type="submit">Add</button>
      </form>

      <div>{entryKeys.length} entr{entryKeys.length !== 1 ? 'ies' : 'y'}</div>

      <ul>
        {entryKeys.map((key) => (
          <li key={key}>
            <span>{key}</span>
            <input
              type="text"
              value={entriesMap[key]}
              onChange={(e) => handleUpdateEntry(key, e.target.value)}
            />
            <button onClick={() => handleDeleteEntry(key)}>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>
  );
};