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:
LocalOnly collections are included in the core TanStack DB package:
npm install @tanstack/react-db
npm install @tanstack/react-db
import { createCollection } from '@tanstack/react-db'
import { localOnlyCollectionOptions } from '@tanstack/react-db'
const uiStateCollection = createCollection(
localOnlyCollectionOptions({
id: 'ui-state',
getKey: (item) => item.id,
})
)
import { createCollection } from '@tanstack/react-db'
import { localOnlyCollectionOptions } from '@tanstack/react-db'
const uiStateCollection = createCollection(
localOnlyCollectionOptions({
id: 'ui-state',
getKey: (item) => item.id,
})
)
The localOnlyCollectionOptions function accepts the following options:
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 },
],
})
)
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)
},
})
)
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 { 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 } 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((q) =>
q.from({ modal: modalStateCollection })
.where(({ modal }) => 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 } 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((q) =>
q.from({ modal: modalStateCollection })
.where(({ modal }) => 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 } 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((q) =>
q.from({ draft: formDraftsCollection })
.where(({ draft }) => 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>
)
}
import { createCollection } 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((q) =>
q.from({ draft: formDraftsCollection })
.where(({ draft }) => 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:
| 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 |
| Best for | Temporary UI state | User preferences |
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.
