Skip to Content
Jumentix DocsPackages@jumentix/canaCana with React Context API

Cana with React Context API

Build a categorized task system from an empty React app to a working offline implementation. The app stores categories and tasks in Cana, listens to committed Cana events, and updates React state through a Context provider.

1. Start from zero

bun create vite cana-react-context --template react-ts cd cana-react-context bun add @jumentix/cana @jumentix/cana-react

Use two stores:

  • categories: task buckets with id, name, color, timestamps.
  • tasks: records with categoryId, completed, priority, timestamps.

Indexes make the UI cheap to reload:

  • categories.byName
  • tasks.byCategory
  • tasks.byCompleted
  • tasks.byUpdatedAt

2. Simple implementation

Create src/cana.ts, then wrap the app with TasksProvider. The provider opens Cana once, loads the current tables, subscribes to CanaChangeEvent, and uses a reducer to patch component state when writes commit.

The important part is this flow:

  1. UI calls cana.table('tasks').add(...).
  2. Cana commits the write.
  3. client.subscribe((event) => ...) receives the committed event.
  4. The reducer maps created | updated | deleted | cleared to React state.
  5. Components re-render from Context state.
React Context
simple

React Context: Category and Task tables

A provider listens to committed Cana events and updates reducer state.

Download app
{
  "name": "cana-react-context-tasks",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@jumentix/cana": "^0.1.0",
    "@jumentix/cana-react": "^0.1.0",
    "@vitejs/plugin-react": "^5.1.2",
    "typescript": "^6.0.3",
    "vite": "^7.2.7",
    "react": "^19.2.8",
    "react-dom": "^19.2.8"
  },
  "devDependencies": {
    "@types/react": "^19.2.18",
    "@types/react-dom": "^19.2.4"
  }
}

3. Advanced implementation

The advanced version adds the parts you need in a serious offline UI:

  • one readwrite transaction creates a category and its first task together;
  • sinceCursor resumes a listener after a reload;
  • isCanaErrorCode(error, 'NotFound') detects an expired replay window;
  • a full table reload repairs state before resubscribing;
  • originId gives you a safe place to ignore your own echo if you also apply optimistic UI patches.

Inside a transaction, keep the body limited to Cana/IndexedDB work. Do not put fetch, timers, or unrelated async work inside the transaction callback.

React Context
advanced

React Context: replay and transaction flow

The app keeps the same tables and adds replay recovery plus one multi-store transaction.

Download app
{
  "name": "cana-react-context-tasks",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@jumentix/cana": "^0.1.0",
    "@jumentix/cana-react": "^0.1.0",
    "@vitejs/plugin-react": "^5.1.2",
    "typescript": "^6.0.3",
    "vite": "^7.2.7",
    "react": "^19.2.8",
    "react-dom": "^19.2.8"
  },
  "devDependencies": {
    "@types/react": "^19.2.18",
    "@types/react-dom": "^19.2.4"
  }
}

4. Final app shape

The finished Context implementation has this shape:

src/
  cana.ts
  TasksProvider.tsx
  advancedCana.ts
  App.tsx

TasksProvider is the only component that knows how Cana events become React state. Leaf components stay boring: they call addTask and toggleTask, then render state.categories, state.tasks, and state.events.

Download the complete Vite app used by the advanced example: cana-react-context.zip.

5. Checklist

  • open() runs before any table call.
  • The provider unsubscribes in the useEffect cleanup.
  • Components update from committed Cana events.
  • Advanced flows use transaction() for multi-store writes.
  • Replay failure reloads from tables before resubscribing.

Next

Compare this with React Redux for larger apps with explicit slices and selectors, or Vue 3 + Pinia for the Vue store pattern.