0.0.3 • Published 2 months ago

@tanstack/react-optimistic v0.0.3

Weekly downloads
-
License
MIT
Repository
github
Last release
2 months ago

@TanStack/react-optimistic

React hooks and utilities for creating fast optimistic updates with flexible backend support that pairs seamlessly with sync engines (like ElectricSQL).

Installation

pnpm add @TanStack/react-optimistic

Overview

@TanStack/react-optimistic provides React-specific hooks and utilities for managing data synchronization between your frontend application and backend services. It offers:

  • Optimistic Updates: Apply changes instantly in the UI while syncing in the background
  • Flexible Backend Support: Works with any backend or sync engine
  • Immutable Snapshots: Create immutable snapshots of updates that can be persisted and rolled back
  • React Integration: Seamless integration with React components and state management

React Hooks

useCollection

The primary hook for interacting with collections in React components.

const { data, insert, update, delete: deleteFn } = useCollection({
  id: 'todos',
  sync: { /* sync configuration */ },
  mutationFn: { /* mutation functions */ },
  schema: /* optional schema */
});

Returns:

  • data: An array of all items in the collection
  • state: A Map containing all items in the collection with their internal keys
  • insert: Function to add new items to the collection
  • update: Function to modify existing items
  • delete: Function to remove items from the collection

preloadCollection

Preloads data for a collection before rendering components.

await preloadCollection({
  id: 'todos',
  sync: { /* sync configuration */ },
  mutationFn: { /* mutation functions */ },
  schema: /* optional schema */
});

Features:

  1. Returns a promise that resolves when the first sync commit is complete
  2. Shares the same collection instance with useCollection
  3. Handles already-loaded collections by returning immediately
  4. Avoids duplicate initialization when called multiple times with the same ID

Data Operations

Insert

// Insert a single item
insert({ text: "Buy groceries", completed: false })

// Insert multiple items
insert([
  { text: "Buy groceries", completed: false },
  { text: "Walk dog", completed: false },
])

// Insert with custom key
insert({ text: "Buy groceries" }, { key: "grocery-task" })

Update

We use a proxy to capture updates as immutable draft optimistic updates.

// Update a single item
update(todo, (draft) => {
  draft.completed = true
})

// Update multiple items
update([todo1, todo2], (drafts) => {
  drafts.forEach((draft) => {
    draft.completed = true
  })
})

// Update with metadata
update(todo, { metadata: { reason: "user update" } }, (draft) => {
  draft.text = "Updated text"
})

Delete

// Delete a single item
delete todo

// Delete multiple items
delete [todo1, todo2]

// Delete with metadata
delete (todo, { metadata: { reason: "completed" } })

Implementing Backend Integration with ElectricSQL

The mutationFn property is where you define how your application interacts with your backend. Here's a comprehensive example of integrating with ElectricSQL:

import { useCollection } from "@TanStack/react-optimistic"
import { createElectricSync } from "@TanStack/optimistic/electric"

// Create a collection configuration for todos
const todosConfig = {
  id: "todos",
  // Create an ElectricSQL sync configuration
  sync: createElectricSync(
    {
      // ShapeStream options
      url: `http://localhost:3000/v1/shape`,
      params: {
        table: "todos",
      },
    },
    {
      // Primary key for the todos table
      primaryKey: ['id'],
    }
  ),
}

// Use the collection in a component
function TodoList() {
  const { data, insert, update, delete: deleteFn } = useCollection(todosConfig)

  // Create a mutation for handling all todo operations
  const todoMutation = useOptimisticMutation({
    mutationFn: async ({ transaction }) => {
      const payload = transaction.mutations.map(m => {
        const { collection, ...payload } = m
        return payload
      })

      const response = await fetch(`http://localhost:3001/api/mutations`, {
        method: `POST`,
        headers: {
          "Content-Type": `application/json`,
        },
        body: JSON.stringify(payload),
      })

      if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`)
      }

      const result = await response.json()
      await transaction.mutations[0].collection.config.sync.awaitTxid(result.txid)
    }
  })

  const addTodo = () => {
    todoMutation.mutate(() => {
      insert({ title: 'New todo', completed: false })
    })
  }

  const toggleTodo = (todo) => {
    todoMutation.mutate(() => {
      update(todo, (draft) => {
        draft.completed = !draft.completed
      })
    })
  }

  const removeTodo = (todo) => {
    todoMutation.mutate(() => {
      deleteFn(todo)
    })
  }

  return (
    <div>
      <button
        onClick={addTodo}
        disabled={todoMutation.isPending}
      >
        {todoMutation.isPending ? 'Saving...' : 'Add Todo'}
      </button>
      <ul>
        {data.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo)}
              disabled={todoMutation.isPending}
            />
            {todo.title}
            <button
              onClick={() => removeTodo(todo)}
              disabled={todoMutation.isPending}
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  )
}