LocalStorage collections store small amounts of local-only state that persists across browser sessions and syncs across browser tabs in real-time.
The localStorageCollectionOptions allows you to create collections that:
LocalStorage 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 { localStorageCollectionOptions } from '@tanstack/react-db'
const userPreferencesCollection = createCollection(
localStorageCollectionOptions({
id: 'user-preferences',
storageKey: 'app-user-prefs',
getKey: (item) => item.id,
})
)
import { createCollection } from '@tanstack/react-db'
import { localStorageCollectionOptions } from '@tanstack/react-db'
const userPreferencesCollection = createCollection(
localStorageCollectionOptions({
id: 'user-preferences',
storageKey: 'app-user-prefs',
getKey: (item) => item.id,
})
)
The localStorageCollectionOptions function accepts the following options:
LocalStorage collections automatically sync across browser tabs in real-time:
const settingsCollection = createCollection(
localStorageCollectionOptions({
id: 'settings',
storageKey: 'app-settings',
getKey: (item) => item.id,
})
)
// Changes in one tab are automatically reflected in all other tabs
// This works automatically via storage events
const settingsCollection = createCollection(
localStorageCollectionOptions({
id: 'settings',
storageKey: 'app-settings',
getKey: (item) => item.id,
})
)
// Changes in one tab are automatically reflected in all other tabs
// This works automatically via storage events
You can use sessionStorage instead of localStorage for session-only persistence:
const sessionCollection = createCollection(
localStorageCollectionOptions({
id: 'session-data',
storageKey: 'session-key',
storage: sessionStorage, // Use sessionStorage instead
getKey: (item) => item.id,
})
)
const sessionCollection = createCollection(
localStorageCollectionOptions({
id: 'session-data',
storageKey: 'session-key',
storage: sessionStorage, // Use sessionStorage instead
getKey: (item) => item.id,
})
)
Provide any storage implementation that matches the localStorage API:
// Example: Custom storage wrapper with encryption
const encryptedStorage = {
getItem(key: string) {
const encrypted = localStorage.getItem(key)
return encrypted ? decrypt(encrypted) : null
},
setItem(key: string, value: string) {
localStorage.setItem(key, encrypt(value))
},
removeItem(key: string) {
localStorage.removeItem(key)
},
}
const secureCollection = createCollection(
localStorageCollectionOptions({
id: 'secure-data',
storageKey: 'encrypted-key',
storage: encryptedStorage,
getKey: (item) => item.id,
})
)
// Example: Custom storage wrapper with encryption
const encryptedStorage = {
getItem(key: string) {
const encrypted = localStorage.getItem(key)
return encrypted ? decrypt(encrypted) : null
},
setItem(key: string, value: string) {
localStorage.setItem(key, encrypt(value))
},
removeItem(key: string) {
localStorage.removeItem(key)
},
}
const secureCollection = createCollection(
localStorageCollectionOptions({
id: 'secure-data',
storageKey: 'encrypted-key',
storage: encryptedStorage,
getKey: (item) => item.id,
})
)
The storageEventApi option (defaults to window) allows the collection to subscribe to storage events for cross-tab synchronization. A custom storage implementation can provide this API to enable custom cross-tab, cross-window, or cross-process sync:
// Example: Custom storage event API for cross-process sync
const customStorageEventApi = {
addEventListener(event: string, handler: (e: StorageEvent) => void) {
// Custom event subscription logic
// Could be IPC, WebSocket, or any other mechanism
myCustomEventBus.on('storage-change', handler)
},
removeEventListener(event: string, handler: (e: StorageEvent) => void) {
myCustomEventBus.off('storage-change', handler)
},
}
const syncedCollection = createCollection(
localStorageCollectionOptions({
id: 'synced-data',
storageKey: 'data-key',
storage: customStorage,
storageEventApi: customStorageEventApi, // Custom event API
getKey: (item) => item.id,
})
)
// Example: Custom storage event API for cross-process sync
const customStorageEventApi = {
addEventListener(event: string, handler: (e: StorageEvent) => void) {
// Custom event subscription logic
// Could be IPC, WebSocket, or any other mechanism
myCustomEventBus.on('storage-change', handler)
},
removeEventListener(event: string, handler: (e: StorageEvent) => void) {
myCustomEventBus.off('storage-change', handler)
},
}
const syncedCollection = createCollection(
localStorageCollectionOptions({
id: 'synced-data',
storageKey: 'data-key',
storage: customStorage,
storageEventApi: customStorageEventApi, // Custom event API
getKey: (item) => item.id,
})
)
This enables synchronization across different contexts beyond just browser tabs, such as:
Mutation handlers are completely optional. Data will persist to localStorage whether or not you provide handlers:
const preferencesCollection = createCollection(
localStorageCollectionOptions({
id: 'preferences',
storageKey: 'user-prefs',
getKey: (item) => item.id,
// Optional: Add custom logic when preferences are updated
onUpdate: async ({ transaction }) => {
const { modified } = transaction.mutations[0]
console.log('Preference updated:', modified)
// Maybe send analytics or trigger other side effects
},
})
)
const preferencesCollection = createCollection(
localStorageCollectionOptions({
id: 'preferences',
storageKey: 'user-prefs',
getKey: (item) => item.id,
// Optional: Add custom logic when preferences are updated
onUpdate: async ({ transaction }) => {
const { modified } = transaction.mutations[0]
console.log('Preference updated:', modified)
// Maybe send analytics or trigger other side effects
},
})
)
When using LocalStorage 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(
localStorageCollectionOptions({
id: 'form-draft',
storageKey: 'draft-data',
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, persist 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(
localStorageCollectionOptions({
id: 'form-draft',
storageKey: 'draft-data',
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, persist 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 { localStorageCollectionOptions } from '@tanstack/react-db'
import { useLiveQuery } from '@tanstack/react-db'
import { z } from 'zod'
// Define schema
const userPrefsSchema = z.object({
id: z.string(),
theme: z.enum(['light', 'dark', 'auto']),
language: z.string(),
notifications: z.boolean(),
})
type UserPrefs = z.infer<typeof userPrefsSchema>
// Create collection
export const userPreferencesCollection = createCollection(
localStorageCollectionOptions({
id: 'user-preferences',
storageKey: 'app-user-prefs',
getKey: (item) => item.id,
schema: userPrefsSchema,
})
)
// Use in component
function SettingsPanel() {
const { data: prefs } = useLiveQuery((q) =>
q.from({ pref: userPreferencesCollection })
.where(({ pref }) => pref.id === 'current-user')
)
const currentPrefs = prefs[0]
const updateTheme = (theme: 'light' | 'dark' | 'auto') => {
if (currentPrefs) {
userPreferencesCollection.update(currentPrefs.id, (draft) => {
draft.theme = theme
})
} else {
userPreferencesCollection.insert({
id: 'current-user',
theme,
language: 'en',
notifications: true,
})
}
}
return (
<div>
<h2>Theme: {currentPrefs?.theme}</h2>
<button onClick={() => updateTheme('dark')}>Dark Mode</button>
<button onClick={() => updateTheme('light')}>Light Mode</button>
</div>
)
}
import { createCollection } from '@tanstack/react-db'
import { localStorageCollectionOptions } from '@tanstack/react-db'
import { useLiveQuery } from '@tanstack/react-db'
import { z } from 'zod'
// Define schema
const userPrefsSchema = z.object({
id: z.string(),
theme: z.enum(['light', 'dark', 'auto']),
language: z.string(),
notifications: z.boolean(),
})
type UserPrefs = z.infer<typeof userPrefsSchema>
// Create collection
export const userPreferencesCollection = createCollection(
localStorageCollectionOptions({
id: 'user-preferences',
storageKey: 'app-user-prefs',
getKey: (item) => item.id,
schema: userPrefsSchema,
})
)
// Use in component
function SettingsPanel() {
const { data: prefs } = useLiveQuery((q) =>
q.from({ pref: userPreferencesCollection })
.where(({ pref }) => pref.id === 'current-user')
)
const currentPrefs = prefs[0]
const updateTheme = (theme: 'light' | 'dark' | 'auto') => {
if (currentPrefs) {
userPreferencesCollection.update(currentPrefs.id, (draft) => {
draft.theme = theme
})
} else {
userPreferencesCollection.insert({
id: 'current-user',
theme,
language: 'en',
notifications: true,
})
}
}
return (
<div>
<h2>Theme: {currentPrefs?.theme}</h2>
<button onClick={() => updateTheme('dark')}>Dark Mode</button>
<button onClick={() => updateTheme('light')}>Light Mode</button>
</div>
)
}
LocalStorage collections are perfect for:
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.
