Skip to content

List Ordering

Fractional indexing enables conflict-free list ordering in distributed systems by assigning string-based position values that maintain lexicographic order. This makes it ideal for implementing drag-and-drop reordering in LiveStore applications where multiple clients may reorder items concurrently.

To understand how the algorithm works, see these visual and interactive explanations:

Traditional numeric ordering (1, 2, 3…) requires renumbering multiple items when inserting or reordering, which creates conflicts in distributed systems. Fractional indexing solves this by:

  • Generating position values that can always be inserted between any two existing positions
  • Using lexicographic string ordering that works naturally with SQL ORDER BY
  • Eliminating the need for coordination between clients when reordering
  • Avoiding cascading updates when inserting items

For example, inserting a new item between positions “a0” and “b0” generates “aV”, which sorts correctly without modifying existing items.

Install the fractional-indexing package from Rocicorp (the same team behind Replicache):

Terminal window
pnpm install fractional-indexing

Define a text column to store the fractional index and add a database index for efficient ordering:

import {
import State
State
} from '@livestore/livestore'
export const
const task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", {
readonly id: {
columnType: "integer";
schema: Codec<number, number, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
};
readonly title: {
columnType: "text";
schema: Codec<string, string, never, never>;
default: Some<"">;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly completed: {
columnType: "integer";
schema: Codec<number, number, never, never>;
default: Some<0>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly order: {
columnType: "text";
schema: Codec<string, string, never, never>;
default: Some<"">;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
}>, State.SQLite.WithDefaults<...>, Struct<...>>
task
=
import State
State
.
import SQLite
SQLite
.
function table<"task", {
readonly id: {
columnType: "integer";
schema: Codec<number, number, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
};
readonly title: {
columnType: "text";
schema: Codec<string, string, never, never>;
default: Some<"">;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly completed: {
columnType: "integer";
schema: Codec<number, number, never, never>;
default: Some<0>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly order: {
columnType: "text";
schema: Codec<string, string, never, never>;
default: Some<"">;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
}, {
...;
}>(args: {
...;
} & Partial<...>): State.SQLite.TableDef<...> (+2 overloads)

Creates a SQLite table definition from columns or an Effect Schema.

This function supports two main ways to define a table:

  1. Using explicit column definitions
  2. Using an Effect Schema (either the name property needs to be provided or the schema needs to have a title/identifier)
// Using explicit columns
const usersTable = State.SQLite.table({
name: 'users',
columns: {
id: State.SQLite.text({ primaryKey: true }),
name: State.SQLite.text({ nullable: false }),
email: State.SQLite.text({ nullable: false }),
age: State.SQLite.integer({ nullable: true }),
},
})
// Using Effect Schema with annotations
import { Schema } from '@livestore/utils/effect'
const UserSchema = Schema.Struct({
id: Schema.Int.pipe(State.SQLite.withPrimaryKey).pipe(State.SQLite.withAutoIncrement),
email: Schema.String.pipe(State.SQLite.withUnique),
name: Schema.String,
active: Schema.Boolean.pipe(State.SQLite.withDefault(true)),
createdAt: Schema.optional(Schema.Date),
})
// Option 1: With explicit name
const usersTable = State.SQLite.table({
name: 'users',
schema: UserSchema,
})
// Option 2: With name from schema annotation (title or identifier)
const AnnotatedUserSchema = UserSchema.annotate({ title: 'users' })
const usersTable2 = State.SQLite.table({
schema: AnnotatedUserSchema,
})
// Adding indexes
const PostSchema = Schema.Struct({
id: Schema.String.pipe(State.SQLite.withPrimaryKey),
title: Schema.String,
authorId: Schema.String,
createdAt: Schema.Date,
}).annotate({ identifier: 'posts' })
const postsTable = State.SQLite.table({
schema: PostSchema,
indexes: [
{ name: 'idx_posts_author', columns: ['authorId'] },
{ name: 'idx_posts_created', columns: ['createdAt'], isUnique: false },
],
})

table
({
name: "task"
name
: 'task',
columns: {
readonly id: {
columnType: "integer";
schema: Codec<number, number, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
};
readonly title: {
columnType: "text";
schema: Codec<string, string, never, never>;
default: Some<"">;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly completed: {
columnType: "integer";
schema: Codec<number, number, never, never>;
default: Some<0>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly order: {
columnType: "text";
schema: Codec<string, string, never, never>;
default: Some<"">;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
}
columns
: {
id: {
columnType: "integer";
schema: Codec<number, number, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
}
id
:
import State
State
.
import SQLite
SQLite
.
const integer: <number, number, false, typeof NoDefault, true, false>(args: {
schema?: Codec<number, number, never, never>;
default?: typeof NoDefault;
nullable?: false;
primaryKey?: true;
autoIncrement?: false;
}) => {
columnType: "integer";
schema: Codec<number, number, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
} (+1 overload)
integer
({
primaryKey?: true
primaryKey
: true }),
title: {
columnType: "text";
schema: Codec<string, string, never, never>;
default: Some<"">;
nullable: false;
primaryKey: false;
autoIncrement: false;
}
title
:
import State
State
.
import SQLite
SQLite
.
const text: <string, string, false, "", false, false>(args: {
schema?: Codec<string, string, never, never>;
default?: "";
nullable?: false;
primaryKey?: false;
autoIncrement?: false;
}) => {
columnType: "text";
schema: Codec<string, string, never, never>;
default: Some<"">;
nullable: false;
primaryKey: false;
autoIncrement: false;
} (+1 overload)
text
({
default?: ""
default
: '' }),
completed: {
columnType: "integer";
schema: Codec<number, number, never, never>;
default: Some<0>;
nullable: false;
primaryKey: false;
autoIncrement: false;
}
completed
:
import State
State
.
import SQLite
SQLite
.
const integer: <number, number, false, 0, false, false>(args: {
schema?: Codec<number, number, never, never>;
default?: 0;
nullable?: false;
primaryKey?: false;
autoIncrement?: false;
}) => {
columnType: "integer";
schema: Codec<number, number, never, never>;
default: Some<0>;
nullable: false;
primaryKey: false;
autoIncrement: false;
} (+1 overload)
integer
({
default?: 0
default
: 0 }),
/** Fractional index for ordering tasks in the list */
order: {
columnType: "text";
schema: Codec<string, string, never, never>;
default: Some<"">;
nullable: false;
primaryKey: false;
autoIncrement: false;
}

Fractional index for ordering tasks in the list

order
:
import State
State
.
import SQLite
SQLite
.
const text: <string, string, false, "", false, false>(args: {
schema?: Codec<string, string, never, never>;
default?: "";
nullable?: false;
primaryKey?: false;
autoIncrement?: false;
}) => {
columnType: "text";
schema: Codec<string, string, never, never>;
default: Some<"">;
nullable: false;
primaryKey: false;
autoIncrement: false;
} (+1 overload)
text
({
nullable?: false
nullable
: false,
default?: ""
default
: '' }),
},
indexes?: [{
readonly name: "task_order";
readonly columns: readonly ["order"];
}]
indexes
: [
/** Index for efficient ordering queries */
{
name: "task_order"
name
: 'task_order',
columns: readonly ["order"]
columns
: ['order'] },
],
deriveEvents?: true
deriveEvents
: true,
})
export type
type Task = {
readonly id: number;
readonly title: string;
readonly completed: number;
readonly order: string;
}
Task
= typeof
const task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", {
readonly id: {
columnType: "integer";
schema: Codec<number, number, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
};
readonly title: {
columnType: "text";
schema: Codec<string, string, never, never>;
default: Some<"">;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly completed: {
columnType: "integer";
schema: Codec<number, number, never, never>;
default: Some<0>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly order: {
columnType: "text";
schema: Codec<string, string, never, never>;
default: Some<"">;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
}>, State.SQLite.WithDefaults<...>, Struct<...>>
task
.
type Type: Struct.ReadonlySide<{
readonly id: Codec<number, number, never, never>;
readonly title: Codec<string, string, never, never>;
readonly completed: Codec<number, number, never, never>;
readonly order: Codec<string, string, never, never>;
}, "Type">
Type
export const
const tables: {
task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", {
readonly id: {
columnType: "integer";
schema: Codec<number, number, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
};
readonly title: {
columnType: "text";
schema: Codec<string, string, never, never>;
default: Some<"">;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly completed: {
columnType: "integer";
schema: Codec<number, number, never, never>;
default: Some<0>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly order: {
columnType: "text";
schema: Codec<string, string, never, never>;
default: Some<"">;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
}>, State.SQLite.WithDefaults<...>, Struct<...>>;
}
tables
= {
task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", {
readonly id: {
columnType: "integer";
schema: Codec<number, number, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
};
readonly title: {
columnType: "text";
schema: Codec<string, string, never, never>;
default: Some<"">;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly completed: {
columnType: "integer";
schema: Codec<number, number, never, never>;
default: Some<0>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly order: {
columnType: "text";
schema: Codec<string, string, never, never>;
default: Some<"">;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
}>, State.SQLite.WithDefaults<...>, Struct<...>>
task
}

The order column stores string values like “a0”, “a1”, “aV” that maintain lexicographic ordering. Adding a database index on this column ensures efficient queries when retrieving ordered lists.

Use generateKeyBetween(a, b) to generate position values when creating new items:

import {
function generateKeyBetween(a: string | null | undefined, b: string | null | undefined, digits?: string | undefined): string

@parama

@paramb

@paramdigits

generateKeyBetween
} from 'fractional-indexing'
import type {
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
} from '@livestore/livestore'
import {
import events
events
} from './events.ts'
import {
import tables
tables
} from './schema.ts'
declare const
const store: Store<LiveStoreSchema.Any, {}>
store
:
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
/** Create a new task at the end of the list */
export const
const createTaskAtEnd: (title: string) => string

Create a new task at the end of the list

createTaskAtEnd
= (
title: string
title
: string) => {
// Get the highest order value
const
const highestOrder: any
highestOrder
=
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.query: <unknown>(query: Queryable<unknown> | {
query: string;
bindValues: Bindable;
schema?: Decoder<unknown, never>;
}, options?: {
otelContext?: Context;
debugRefreshReason?: RefreshReason;
}) => unknown

Synchronously queries the database without creating a LiveQuery. This is useful for queries that don't need to be reactive.

Example: Query builder

const completedTodos = store.query(tables.todo.where({ complete: true }))

Example: Raw SQL query

const completedTodos = store.query({ query: 'SELECT * FROM todo WHERE complete = 1', bindValues: {} })

query
(
import tables
tables
.
any
task
.
any
select
('order').
any
orderBy
('order', 'desc').
any
limit
(1))[0] ?? null
// Generate new order after the highest
const
const order: string
order
=
function generateKeyBetween(a: string | null | undefined, b: string | null | undefined, digits?: string | undefined): string

@parama

@paramb

@paramdigits

generateKeyBetween
(
const highestOrder: any
highestOrder
, null)
// Commit the event
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.commit: <readonly [any]>(list_0: any) => void (+3 overloads)
commit
(
import events
events
.
any
createTask
({
title: string
title
,
order: string
order
}))
return
const order: string
order
}
/** Create a new task at the beginning of the list */
export const
const createTaskAtStart: (title: string) => string

Create a new task at the beginning of the list

createTaskAtStart
= (
title: string
title
: string) => {
// Get the lowest order value
const
const lowestOrder: any
lowestOrder
=
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.query: <unknown>(query: Queryable<unknown> | {
query: string;
bindValues: Bindable;
schema?: Decoder<unknown, never>;
}, options?: {
otelContext?: Context;
debugRefreshReason?: RefreshReason;
}) => unknown

Synchronously queries the database without creating a LiveQuery. This is useful for queries that don't need to be reactive.

Example: Query builder

const completedTodos = store.query(tables.todo.where({ complete: true }))

Example: Raw SQL query

const completedTodos = store.query({ query: 'SELECT * FROM todo WHERE complete = 1', bindValues: {} })

query
(
import tables
tables
.
any
task
.
any
select
('order').
any
orderBy
('order', 'asc').
any
limit
(1))[0] ?? null
// Generate new order before the lowest
const
const order: string
order
=
function generateKeyBetween(a: string | null | undefined, b: string | null | undefined, digits?: string | undefined): string

@parama

@paramb

@paramdigits

generateKeyBetween
(null,
const lowestOrder: any
lowestOrder
)
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.commit: <readonly [any]>(list_0: any) => void (+3 overloads)
commit
(
import events
events
.
any
createTask
({
title: string
title
,
order: string
order
}))
return
const order: string
order
}
/** Create the very first task in an empty list */
export const
const createFirstTask: (title: string) => string

Create the very first task in an empty list

createFirstTask
= (
title: string
title
: string) => {
// When the list is empty, use a simple default value
const
const order: "a1"
order
= 'a1'
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.commit: <readonly [any]>(list_0: any) => void (+3 overloads)
commit
(
import events
events
.
any
createTask
({
title: string
title
,
order: string
order
}))
return
const order: "a1"
order
}

Key patterns:

  • generateKeyBetween(highest, null) - Append to end of list
  • generateKeyBetween(null, lowest) - Prepend to start of list
  • Use "a1" as a simple default for the first item in an empty list

Handle drag-and-drop by generating a new position between the target boundaries:

import {
function generateKeyBetween(a: string | null | undefined, b: string | null | undefined, digits?: string | undefined): string

@parama

@paramb

@paramdigits

generateKeyBetween
} from 'fractional-indexing'
import type {
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
} from '@livestore/livestore'
import {
import events
events
} from './events.ts'
import {
import tables
tables
} from './schema.ts'
declare const
const store: Store<LiveStoreSchema.Any, {}>
store
:
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
/**
* Reorder a task by moving it between two other tasks
*
* @param taskId - The task to reorder
* @param beforeOrder - The order value of the task that will be before this one (null if moving to end)
* @param afterOrder - The order value of the task that will be after this one (null if moving to start)
*/
export const
const reorderTask: (taskId: number, beforeOrder: string | null, afterOrder: string | null) => string

Reorder a task by moving it between two other tasks

@paramtaskId - The task to reorder

@parambeforeOrder - The order value of the task that will be before this one (null if moving to end)

@paramafterOrder - The order value of the task that will be after this one (null if moving to start)

reorderTask
= (
taskId: number

  • The task to reorder

@paramtaskId - The task to reorder

taskId
: number,
beforeOrder: string | null

  • The order value of the task that will be before this one (null if moving to end)

@parambeforeOrder - The order value of the task that will be before this one (null if moving to end)

beforeOrder
: string | null,
afterOrder: string | null

  • The order value of the task that will be after this one (null if moving to start)

@paramafterOrder - The order value of the task that will be after this one (null if moving to start)

afterOrder
: string | null) => {
// Generate a new fractional index between the two positions
const
const newOrder: string
newOrder
=
function generateKeyBetween(a: string | null | undefined, b: string | null | undefined, digits?: string | undefined): string

@parama

@paramb

@paramdigits

generateKeyBetween
(
beforeOrder: string | null

  • The order value of the task that will be before this one (null if moving to end)

@parambeforeOrder - The order value of the task that will be before this one (null if moving to end)

beforeOrder
,
afterOrder: string | null

  • The order value of the task that will be after this one (null if moving to start)

@paramafterOrder - The order value of the task that will be after this one (null if moving to start)

afterOrder
)
// Commit the update event
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.commit: <readonly [any]>(list_0: any) => void (+3 overloads)
commit
(
import events
events
.
any
updateTaskOrder
({
id: number
id
:
taskId: number

  • The task to reorder

@paramtaskId - The task to reorder

taskId
,
order: string
order
:
const newOrder: string
newOrder
}))
return
const newOrder: string
newOrder
}
/**
* Handle drag-and-drop reordering
*
* This is a more complete example showing how to handle drag-and-drop
* with proper boundary checks.
*/
export const
const handleDragDrop: (draggedTaskId: number, targetTaskId: number, dropPosition: "before" | "after") => string

Handle drag-and-drop reordering

This is a more complete example showing how to handle drag-and-drop with proper boundary checks.

handleDragDrop
= (
draggedTaskId: number
draggedTaskId
: number,
targetTaskId: number
targetTaskId
: number,
dropPosition: "before" | "after"
dropPosition
: 'before' | 'after') => {
const
const before: boolean
before
=
dropPosition: "before" | "after"
dropPosition
=== 'before'
// Get the target task's order
const
const targetOrder: unknown
targetOrder
=
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.query: <unknown>(query: Queryable<unknown> | {
query: string;
bindValues: Bindable;
schema?: Decoder<unknown, never>;
}, options?: {
otelContext?: Context;
debugRefreshReason?: RefreshReason;
}) => unknown

Synchronously queries the database without creating a LiveQuery. This is useful for queries that don't need to be reactive.

Example: Query builder

const completedTodos = store.query(tables.todo.where({ complete: true }))

Example: Raw SQL query

const completedTodos = store.query({ query: 'SELECT * FROM todo WHERE complete = 1', bindValues: {} })

query
(
import tables
tables
.
any
task
.
any
select
('order').
any
where
({
id: number
id
:
targetTaskId: number
targetTaskId
}).
any
first
({
behaviour: string
behaviour
: 'error' }))
// Find the nearest task in the drop direction
const
const nearestOrder: any
nearestOrder
=
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.query: <unknown>(query: Queryable<unknown> | {
query: string;
bindValues: Bindable;
schema?: Decoder<unknown, never>;
}, options?: {
otelContext?: Context;
debugRefreshReason?: RefreshReason;
}) => unknown

Synchronously queries the database without creating a LiveQuery. This is useful for queries that don't need to be reactive.

Example: Query builder

const completedTodos = store.query(tables.todo.where({ complete: true }))

Example: Raw SQL query

const completedTodos = store.query({ query: 'SELECT * FROM todo WHERE complete = 1', bindValues: {} })

query
(
import tables
tables
.
any
task
.
any
select
('order')
.
any
where
({
order: {
op: string;
value: unknown;
}
order
: {
op: string
op
:
const before: boolean
before
=== true ? '>' : '<',
value: unknown
value
:
const targetOrder: unknown
targetOrder
},
})
.
any
orderBy
('order',
const before: boolean
before
=== true ? 'asc' : 'desc')
.
any
limit
(1),
)[0] ?? null
// Generate new order between target and nearest
const
const newOrder: string
newOrder
=
function generateKeyBetween(a: string | null | undefined, b: string | null | undefined, digits?: string | undefined): string

@parama

@paramb

@paramdigits

generateKeyBetween
(
const before: boolean
before
=== true ?
const targetOrder: unknown
targetOrder
:
const nearestOrder: any
nearestOrder
,
const before: boolean
before
=== true ?
const nearestOrder: any
nearestOrder
:
const targetOrder: unknown
targetOrder
,
)
// Commit the update
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.commit: <readonly [any]>(list_0: any) => void (+3 overloads)
commit
(
import events
events
.
any
updateTaskOrder
({
id: number
id
:
draggedTaskId: number
draggedTaskId
,
order: string
order
:
const newOrder: string
newOrder
}))
return
const newOrder: string
newOrder
}

The generateKeyBetween function automatically creates a string value that sorts lexicographically between the two boundary positions. Pass null to represent the start or end of the list.

Query items using standard SQL ordering on the fractional index column:

import type {
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
} from '@livestore/livestore'
import {
import tables
tables
} from './schema.ts'
declare const
const store: Store<LiveStoreSchema.Any, {}>
store
:
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
/**
* Query tasks in their proper order
*
* The fractional index values maintain lexicographic ordering,
* so we can simply order by the 'order' column.
*/
export const
const getOrderedTasks: () => unknown

Query tasks in their proper order

The fractional index values maintain lexicographic ordering, so we can simply order by the 'order' column.

getOrderedTasks
= () => {
return
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.query: <unknown>(query: Queryable<unknown> | {
query: string;
bindValues: Bindable;
schema?: Decoder<unknown, never>;
}, options?: {
otelContext?: Context;
debugRefreshReason?: RefreshReason;
}) => unknown

Synchronously queries the database without creating a LiveQuery. This is useful for queries that don't need to be reactive.

Example: Query builder

const completedTodos = store.query(tables.todo.where({ complete: true }))

Example: Raw SQL query

const completedTodos = store.query({ query: 'SELECT * FROM todo WHERE complete = 1', bindValues: {} })

query
(
import tables
tables
.
any
task
.
any
select
().
any
orderBy
('order', 'asc'))
}
/**
* Get the highest order value (for appending new items)
*/
export const
const getHighestOrder: () => string | null

Get the highest order value (for appending new items)

getHighestOrder
= (): string | null => {
const
const order: any
order
=
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.query: <unknown>(query: Queryable<unknown> | {
query: string;
bindValues: Bindable;
schema?: Decoder<unknown, never>;
}, options?: {
otelContext?: Context;
debugRefreshReason?: RefreshReason;
}) => unknown

Synchronously queries the database without creating a LiveQuery. This is useful for queries that don't need to be reactive.

Example: Query builder

const completedTodos = store.query(tables.todo.where({ complete: true }))

Example: Raw SQL query

const completedTodos = store.query({ query: 'SELECT * FROM todo WHERE complete = 1', bindValues: {} })

query
(
import tables
tables
.
any
task
.
any
select
('order').
any
orderBy
('order', 'desc').
any
limit
(1))[0]
return
const order: any
order
?? null
}
/**
* Get the lowest order value (for prepending new items)
*/
export const
const getLowestOrder: () => string | null

Get the lowest order value (for prepending new items)

getLowestOrder
= (): string | null => {
const
const order: any
order
=
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.query: <unknown>(query: Queryable<unknown> | {
query: string;
bindValues: Bindable;
schema?: Decoder<unknown, never>;
}, options?: {
otelContext?: Context;
debugRefreshReason?: RefreshReason;
}) => unknown

Synchronously queries the database without creating a LiveQuery. This is useful for queries that don't need to be reactive.

Example: Query builder

const completedTodos = store.query(tables.todo.where({ complete: true }))

Example: Raw SQL query

const completedTodos = store.query({ query: 'SELECT * FROM todo WHERE complete = 1', bindValues: {} })

query
(
import tables
tables
.
any
task
.
any
select
('order').
any
orderBy
('order', 'asc').
any
limit
(1))[0]
return
const order: any
order
?? null
}

Since fractional index values maintain lexicographic ordering, you can use simple ORDER BY clauses without special handling.

  • Use text/string columns - Fractional index values are strings, not numbers
  • Add database indexes - Index the order column for efficient queries
  • Validate lexicographic ordering - Ensure your sorting logic uses standard string comparison, not locale-aware comparison (avoid String.prototype.localeCompare())
  • Handle empty lists - Use a simple default like "a1" for the first item
  • Consider bulk operations - For creating multiple items at once, use generateNKeysBetween(a, b, n) from the same package

The web-linearlite example demonstrates fractional indexing for Kanban board ordering. It shows:

  • Schema definition with kanbanorder column
  • Creating issues with proper ordering
  • Drag-and-drop reordering across columns
  • Querying issues in order by status

Check examples/web-linearlite/src/livestore/schema/issue.ts for the schema and examples/web-linearlite/src/components/column.tsx for the drag-and-drop implementation.

Fractional indexing generates position strings that always allow insertion between any two positions:

Initial: a0 a1 a2
Insert between a0 and a1: a0V (sorts: a0 < a0V < a1)
Insert between a0V and a1: a0n (sorts: a0 < a0V < a0n < a1)

The algorithm ensures:

  • Position strings stay reasonably short
  • No coordination needed between clients
  • Conflicts resolve naturally through lexicographic ordering
  • Works seamlessly with LiveStore’s event sourcing model

For more details on the algorithm, see the fractional-indexing documentation.