You're viewing the v2.3 docs.
Ledger docs
Introduction

Getting started

Ledger is a tiny reactive store for TypeScript. In about five minutes you'll install it, define a typed collection, write a live query, and render it in React. No server, no build plugin, no config.

Install

Ledger ships as an ESM package with bundled type definitions. Add it with your package manager of choice:

bash
# npm
npm install ledger

# pnpm
pnpm add ledger

# bun
bun add ledger

Ledger requires TypeScript 5.0+ for full inference, but it runs fine in plain JavaScript — you just lose the compile-time checks.

Create a store

A store is a map of named collections. Each collection is typed by the record it holds, and you tell it which field is the primary key.

db.ts typescript
import { createLedger, collection } from 'ledger';

type Note = {
  id: string;
  title: string;
  body: string;
  pinned: boolean;
  createdAt: number;
};

export const db = createLedger({
  notes: collection<Note>({ key: 'id' }),
});

That's the whole schema. db.notes is now a fully-typed collection: TypeScript knows its records are Note objects keyed by id.

Insert and read

Inserts are synchronous and type-checked. Reads come straight from the in-memory index:

typescript
db.notes.insert({
  id: crypto.randomUUID(),
  title: 'Welcome to Ledger',
  body: 'Edit me, delete me, query me.',
  pinned: true,
  createdAt: Date.now(),
});

// Read everything, or read one record by key:
const all = db.notes.all();
const one = db.notes.get('some-id');

Inserting a record whose key already exists throws. Use upsert() if you want insert-or-replace semantics instead.

Go reactive

The point of Ledger is live data. Turn any query into a subscription with .live(), and your callback fires once immediately and again every time the result changes — and only when it actually changes.

typescript
// .live() returns a Query you can subscribe to.
const pinned = db.notes
  .where((n) => n.pinned)
  .sort('createdAt', 'desc')
  .live();

const stop = pinned.subscribe((notes) => {
  console.log('pinned notes changed:', notes.length);
});

// Later, when you're done:
stop();

Wire up React

The ledger/react binding wraps all of that in a single hook. Pass a function that returns a live query; the component re-renders with fresh data and unsubscribes on unmount.

PinnedNotes.tsx tsx
import { useQuery } from 'ledger/react';
import { db } from './db';

export function PinnedNotes() {
  const notes = useQuery(() =>
    db.notes.where((n) => n.pinned).sort('createdAt', 'desc').live()
  );

  return (
    <ul>
      {notes.map((n) => (
        <li key={n.id}>{n.title}</li>
      ))}
    </ul>
  );
}

Vue and Svelte bindings (ledger/vue, ledger/svelte) follow the same shape.

Next steps

You have a working reactive store. From here:

Esc
↑↓ navigate ↵ open Esc close