GoatDB React Hooks

The hooks are built on top of GoatDB’s core functionality, providing a more ergonomic interface for React components. They handle all the complexity of data synchronization and updates, making it easy to build reactive UIs that work seamlessly both online and offline.

Hooks Overview

useDB()

Initializes and returns the default GoatDB instance, handling storage and server synchronization automatically. The hook also bootstraps the database by setting up the storage backend and creating an initial connection to the server. This ensures that the application is ready to interact with the database without requiring additional setup steps.

  • Behavior:
    • Uses the Origin Private File System (OPFS) for storage
    • Synchronizes with the server in the background
    • Triggers re-renders when the current user changes
  • Returns: A GoatDB instance

Example:

const db = useDB();

The useDB hook maintains a single instance of the database throughout your application’s lifecycle. All subsequent calls to useDB will return the same instance, ensuring consistent state management across your components.

useDBReady()

Monitors the database’s loading state. Use it to manage your application’s initial loading screen. During this phase, the client loads locally stored data, establishes a server connection, and initializes an anonymous session if required.

  • Returns:
    • "loading": Database is initializing
    • "ready": Database is fully loaded and synchronized
    • "error": An error occurred during initialization

Example:

function App() {
  const dbStatus = useDBReady();

  if (dbStatus === 'loading') {
    return <LoadingScreen />;
  } else if (dbStatus === 'error') {
    return <ErrorScreen message='Failed to load database.' />;
  }

  return <MainApp />;
}

During the initial session setup, the client may require a network connection in order to download the initial copy of the history. Once this setup is complete, full offline functionality is supported.

useQuery()

Creates a new query or retrieves an existing one. On first access, GoatDB automatically loads the source repository either from the local disk or by fetching it from the server. The hook triggers UI re-rendering whenever the query results are updated, regardless of whether the changes originate from local or remote edits.

When a query is first opened, it performs a linear scan of its source using a coroutine without blocking the main thread. During and after this initial scan, the query caches its results to disk, allowing subsequent runs to resume execution from the cached state.

Config Options:

  • schema (required): Specifies the schema(s) for the query results
  • source (required): Path to a repository or another query instance
  • predicate (optional): Function to filter results
  • sortDescriptor (optional): Function to sort results
  • ctx (optional): Optional context for predicates and sort descriptors
  • limit (optional): Limits the number of results
  • showIntermittentResults (optional): If true, updates UI during initial scan

GoatDB re-evaluates the entire query whenever any of its configuration values change, including:

  • The predicate function
  • The sort descriptor function
  • The context object
  • The source repository
  • The schema

GoatDB internally calls .toString() on the passed functions to determine if they have changed. While you don’t need to explicitly use useCallback or other memoization techniques, it’s crucial that your predicate and sort functions are pure functions:

  • They should not modify any external state
  • They should not depend on values that can change between calls
  • They should not modify the items they receive
  • Use the ctx parameter to pass in any external values needed

Example:

function TaskList() {
  const tasksQuery = useQuery({
    schema: taskSchema,
    source: '/data/tasks',
    sortDescriptor: (a, b) => a.get('text').localeCompare(b.get('text')),
    predicate: (item) => !item.get('done'),
    showIntermittentResults: true,
  });

  return (
    <ul className='task-list'>
      {tasksQuery.results().map((task) => (
        <li key={task.path}>{task.get('text')}</li>
      ))}
    </ul>
  );
}

useItem()

Monitors changes to a specific item, triggering a re-render whenever the item’s state changes. It returns a mutable ManagedItem instance that allows direct modifications. Any changes to the item are automatically queued for background commits and synchronized with the server.

Signatures:

useItem<S extends Schema>(...pathComps: string[]): ManagedItem<S> | undefined
useItem<S extends Schema>(path: string | undefined, opts: UseItemOpts): ManagedItem<S> | undefined
useItem<S extends Schema>(item: ManagedItem<S> | undefined, opts: UseItemOpts): ManagedItem<S> | undefined
  • Options:
    • keys (optional): Array of field names to track. Optimizes rendering by ignoring changes to other fields

Example:

function TaskEditor({ path }) {
  const task = useItem(path, { keys: ['text'] });

  if (!task) {
    return <div>Loading task...</div>;
  }

  return (
    <div className='task-editor'>
      <label htmlFor='task-text'>Task:</label>
      <input
        id='task-text'
        type='text'
        value={task.get('text')}
        onChange={(e) => task.set('text', e.target.value)}
      />
    </div>
  );
}

The useItem hook will automatically trigger a re-render when:

  • The item becomes available after loading
  • Any tracked field changes
  • The schema changes
  • The item is deleted or restored

Best Practices

Performance Optimization

  1. Use keys with useItem: When you only need to track specific fields, use the keys option to prevent unnecessary re-renders.

  2. Memoize Predicates: Define predicate and sort functions outside your component or use useCallback to maintain stable references:

    const userPredicate = useCallback(
      ({ item }) => item.get('active') && item.get('role') === roleFilter,
      [roleFilter],
    );
    
  3. Chain Queries: For complex data transformations, chain queries together rather than performing multiple operations in a single query.

Error Handling

  1. Check for Undefined Items: Always handle the case where useItem returns undefined, which can happen during initial loading or if the item doesn’t exist.

  2. Monitor DB Ready State: Use useDBReady to handle loading and error states gracefully.

Data Synchronization

  1. Background Writes: All writes are processed asynchronously. The system batches changes and writes them to both local storage and remote servers in parallel.

  2. Offline Support: GoatDB maintains a local copy of the database and synchronizes changes when the connection is restored.

Technical Details

The React hooks are implemented using React’s useSyncExternalStore to manage subscriptions to database changes. This ensures efficient updates and proper cleanup when components unmount.

  • Change Detection: The hooks use GoatDB’s mutation system to track changes at the field level, enabling precise updates.

  • Memory Management: Resources are automatically cleaned up when components unmount, preventing memory leaks.

  • Concurrency: The hooks handle concurrent updates gracefully, ensuring consistent state even when multiple components modify the same data.