LocalOnly collections are designed for in-memory client data or UI state that doesn't need to persist across browser sessions or sync across tabs.
The localOnlyCollectionOptions allows you to create collections that:
Store data only in memory (no persistence)
Support optimistic updates with automatic rollback on errors
Provide optional initial data
Work perfectly for temporary UI state and session-only data
Automatically manage the transition from optimistic to confirmed state
LocalOnly collections are included in the core TanStack DB package:
npm install @tanstack/react-dbimport { createCollection } from '@tanstack/react-db'
import { localOnlyCollectionOptions } from '@tanstack/react-db'
const uiStateCollection = createCollection(
localOnlyCollectionOptions({
id: 'ui-state',
getKey: (item) => item.id,
})
)Important: LocalOnly collections work differently than server-synced collections. With LocalOnly collections, you directly mutate state by calling methods like collection.insert(), collection.update(), and collection.delete() — that's all you need to do. The changes are immediately applied to your local in-memory data.
This is different from collections that sync with a server (like Query Collection), where mutation handlers send data to a backend. With LocalOnly collections, everything stays local:
// Just call the methods directly - no server sync involved
uiStateCollection.insert({ id: 'theme', mode: 'dark' })
uiStateCollection.update('theme', (draft) => { draft.mode = 'light' })
uiStateCollection.delete('theme')The localOnlyCollectionOptions function accepts the following options:
id: Unique identifier for the collection
getKey: Function to extract the unique key from an item
schema: Standard Schema compatible schema (e.g., Zod, Effect) for client-side validation
initialData: Array of items to populate the collection with on creation
onInsert: Optional handler function called before confirming inserts
onUpdate: Optional handler function called before confirming updates
onDelete: Optional handler function called before confirming deletes
A local-only Collection keeps rows in memory and never serializes them. A row can hold values that a persisted Collection cannot store, such as functions. The Collection returns such a value unchanged, with the same identity, after inserts and updates. See Mutations for how drafts copy plain objects and keep class instances by reference.
const handlers = createCollection(
localOnlyCollectionOptions({
getKey: (item) => item.id,
}),
)
handlers.insert({ id: 'save', run: () => console.log('saved') })
handlers.get('save')?.run() // logs "saved"Nothing survives a page reload. If you might move these rows to LocalStorageCollection or IndexedDB later, keep rows as plain data and look up behavior outside the row. Those Collections reject values they cannot store.
Populate the collection with initial data on creation:
const uiStateCollection = createCollection(
localOnlyCollectionOptions({
id: 'ui-state',
getKey: (item) => item.id,
initialData: [
{ id: 'sidebar', isOpen: false },
{ id: 'theme', mode: 'light' },
{ id: 'modal', visible: false },
],
})
)Mutation handlers are completely optional. When provided, they are called before the optimistic state is confirmed:
const tempDataCollection = createCollection(
localOnlyCollectionOptions({
id: 'temp-data',
getKey: (item) => item.id,
onInsert: async ({ transaction }) => {
// Custom logic before confirming the insert
console.log('Inserting:', transaction.mutations[0].modified)
},
onUpdate: async ({ transaction }) => {
// Custom logic before confirming the update
const { original, modified } = transaction.mutations[0]
console.log('Updating from', original, 'to', modified)
},
onDelete: async ({ transaction }) => {
// Custom logic before confirming the delete
console.log('Deleting:', transaction.mutations[0].original)
},
})
)When using LocalOnly collections with manual transactions (created via createTransaction), you must call utils.acceptMutations() to persist the changes:
import { createTransaction } from '@tanstack/react-db'
const localData = createCollection(
localOnlyCollectionOptions({
id: 'form-draft',
getKey: (item) => item.id,
})
)
const serverCollection = createCollection(
queryCollectionOptions({
queryKey: ['items'],
queryFn: async () => api.items.getAll(),
getKey: (item) => item.id,
onInsert: async ({ transaction }) => {
await api.items.create(transaction.mutations[0].modified)
},
})
)
const tx = createTransaction({
mutationFn: async ({ transaction }) => {
// Handle server collection mutations explicitly in mutationFn
await Promise.all(
transaction.mutations
.filter((m) => m.collection === serverCollection)
.map((m) => api.items.create(m.modified))
)
// After server mutations succeed, accept local collection mutations
localData.utils.acceptMutations(transaction)
},
})
// Apply mutations to both collections in one transaction
tx.mutate(() => {
localData.insert({ id: 'draft-1', data: '...' })
serverCollection.insert({ id: '1', name: 'Item' })
})
await tx.commit()import { createCollection, eq } from '@tanstack/react-db'
import { localOnlyCollectionOptions } from '@tanstack/react-db'
import { useLiveQuery } from '@tanstack/react-db'
import { z } from 'zod'
// Define schema
const modalStateSchema = z.object({
id: z.string(),
isOpen: z.boolean(),
data: z.any().optional(),
})
type ModalState = z.infer<typeof modalStateSchema>
// Create collection
export const modalStateCollection = createCollection(
localOnlyCollectionOptions({
id: 'modal-state',
getKey: (item) => item.id,
schema: modalStateSchema,
initialData: [
{ id: 'user-profile', isOpen: false },
{ id: 'settings', isOpen: false },
{ id: 'confirm-delete', isOpen: false },
],
})
)
// Use in component
function UserProfileModal() {
const { data: modals } = useLiveQuery({
query: (q) =>
q
.from({ modal: modalStateCollection })
.where(({ modal }) => eq(modal.id, 'user-profile')),
})
const modalState = modals[0]
const openModal = (data?: any) => {
modalStateCollection.update('user-profile', (draft) => {
draft.isOpen = true
draft.data = data
})
}
const closeModal = () => {
modalStateCollection.update('user-profile', (draft) => {
draft.isOpen = false
draft.data = undefined
})
}
if (!modalState?.isOpen) return null
return (
<div className="modal">
<h2>User Profile</h2>
<pre>{JSON.stringify(modalState.data, null, 2)}</pre>
<button onClick={closeModal}>Close</button>
</div>
)
}import { createCollection, eq } from '@tanstack/react-db'
import { localOnlyCollectionOptions } from '@tanstack/react-db'
import { useLiveQuery } from '@tanstack/react-db'
type FormDraft = {
id: string
formData: Record<string, any>
lastModified: Date
}
// Create collection for form drafts
export const formDraftsCollection = createCollection(
localOnlyCollectionOptions({
id: 'form-drafts',
getKey: (item) => item.id,
})
)
// Use in component
function CreatePostForm() {
const { data: drafts } = useLiveQuery({
query: (q) =>
q
.from({ draft: formDraftsCollection })
.where(({ draft }) => eq(draft.id, 'new-post')),
})
const currentDraft = drafts[0]
const updateDraft = (field: string, value: any) => {
if (currentDraft) {
formDraftsCollection.update('new-post', (draft) => {
draft.formData[field] = value
draft.lastModified = new Date()
})
} else {
formDraftsCollection.insert({
id: 'new-post',
formData: { [field]: value },
lastModified: new Date(),
})
}
}
const clearDraft = () => {
if (currentDraft) {
formDraftsCollection.delete('new-post')
}
}
const submitForm = async () => {
if (!currentDraft) return
await api.posts.create(currentDraft.formData)
clearDraft()
}
return (
<form onSubmit={(e) => { e.preventDefault(); submitForm() }}>
<input
value={currentDraft?.formData.title || ''}
onChange={(e) => updateDraft('title', e.target.value)}
/>
<button type="submit">Publish</button>
<button type="button" onClick={clearDraft}>Clear Draft</button>
</form>
)
}LocalOnly collections are perfect for:
Temporary UI state (modals, sidebars, tooltips)
Form draft data during the current session
Client-side computed or derived data
Wizard/multi-step form state
Temporary filters or search state
In-memory caches
| Feature | LocalOnly | LocalStorage |
|---|---|---|
| Persistence | None (in-memory only) | localStorage |
| Cross-tab sync | No | Yes |
| Survives page reload | No | Yes |
| Performance | Fastest | Fast |
| Size limits | Memory limits | ~5-10MB |
| Row values | Any JavaScript value | Values the configured parser can store (JSON by default) |
| Best for | Temporary UI state | User preferences |