The Firebase plugin provides seamless integration with Firebase Realtime Database, including CRUD operations, real-time subscriptions, and field transformations.
Installation
npm install @legendapp/state firebase
Setup
import { initializeApp } from 'firebase/app'
import { configureSyncedFirebase } from '@legendapp/state/sync-plugins/firebase'
const app = initializeApp({
apiKey: 'YOUR_API_KEY',
authDomain: 'your-app.firebaseapp.com',
databaseURL: 'https://your-app.firebaseio.com',
projectId: 'your-app'
})
configureSyncedFirebase({
realtime: true,
requireAuth: true
})
Usage
import { syncedFirebase } from '@legendapp/state/sync-plugins/firebase'
import { getAuth } from 'firebase/auth'
const posts$ = syncedFirebase({
refPath: (uid) => `users/${uid}/posts`,
as: 'object'
})
Configuration
refPath
(uid: string | undefined) => string
required
Function that returns the Firebase database path. Receives current user ID.// User-specific data
refPath: (uid) => `users/${uid}/posts`
// Global data
refPath: () => 'posts'
// Dynamic path
refPath: (uid) => `tenants/${tenantId$.get()}/data`
query
(ref: DatabaseReference) => DatabaseReference | Query
Additional query constraintsimport { orderByChild, limitToLast } from 'firebase/database'
query: (ref) => query(ref, orderByChild('createdAt'), limitToLast(20))
as
'object' | 'array' | 'Map' | 'value'
default:"object"
How to structure the data
object: { [id]: item }
array: [item, item]
Map: Map<id, item>
value: Single value (not a collection)
Field tracking creation time
Field tracking update time (defaults to ’@’ which is special in Firebase)
Map local field names to Firebase field namesfieldTransforms: {
userId: 'user_id',
createdAt: 'created_at'
}
Enable real-time subscriptions
Wait for authentication before syncing
Make observable read-only (no writes to Firebase)
changesSince
'all' | 'last-sync'
default:"all"
Query strategy
Transform data between local and remote formats
Local persistence configuration
Examples
Basic Usage
import { syncedFirebase } from '@legendapp/state/sync-plugins/firebase'
const posts$ = syncedFirebase({
refPath: (uid) => `users/${uid}/posts`,
as: 'object'
})
// Automatically syncs with Firebase
const posts = posts$.get()
Real-time Sync
import { observer } from '@legendapp/state/react'
const messages$ = syncedFirebase({
refPath: () => 'chat/messages',
realtime: true, // Default
as: 'array'
})
const ChatMessages = observer(function ChatMessages() {
const messages = messages$.get()
return (
<div>
{messages.map(msg => (
<div key={msg.id}>{msg.text}</div>
))}
</div>
)
})
// Automatically updates in real-time
CRUD Operations
const posts$ = syncedFirebase({
refPath: (uid) => `users/${uid}/posts`,
as: 'object'
})
// Create
posts$['new-id'].set({
id: 'new-id',
title: 'New Post',
content: 'Content here'
})
// Update
posts$['post-1'].title.set('Updated Title')
// Delete
posts$['post-1'].delete()
// Changes sync to Firebase automatically
With Queries
import { orderByChild, limitToLast } from 'firebase/database'
const recentPosts$ = syncedFirebase({
refPath: () => 'posts',
query: (ref) => query(
ref,
orderByChild('createdAt'),
limitToLast(20)
),
as: 'array'
})
Single Value
// For non-collection data
const settings$ = syncedFirebase({
refPath: (uid) => `users/${uid}/settings`,
as: 'value'
})
// Returns single object (not array/object of items)
const settings = settings$.get()
console.log(settings.theme)
User-Specific Data
import { getAuth } from 'firebase/auth'
const userData$ = syncedFirebase({
refPath: (uid) => `users/${uid}/data`,
requireAuth: true // Wait for auth
})
// Sign in
await getAuth().signInWithEmailAndPassword('[email protected]', 'password')
// Automatically loads user's data
Incremental Sync
const posts$ = syncedFirebase({
refPath: () => 'posts',
changesSince: 'last-sync',
fieldUpdatedAt: '@', // Special Firebase timestamp field
persist: {
name: 'posts',
plugin: ObservablePersistIndexedDB,
retrySync: true
}
})
// Only fetches posts updated since last sync
Server Timestamps
import { serverTimestamp } from 'firebase/database'
const posts$ = syncedFirebase({
refPath: () => 'posts',
fieldCreatedAt: 'createdAt',
fieldUpdatedAt: '@',
as: 'object'
})
// Firebase automatically sets timestamps
posts$['new-post'].set({
id: 'new-post',
title: 'Title'
// createdAt and @ will be set by Firebase
})
import { FieldTransforms } from '@legendapp/state/sync-plugins/firebase'
type LocalPost = {
id: string
userId: string
createdAt: number
}
type FirebasePost = {
id: string
user_id: string // snake_case in Firebase
created_at: number
}
const posts$ = syncedFirebase<FirebasePost, LocalPost>({
refPath: () => 'posts',
fieldTransforms: {
userId: 'user_id', // Local -> Firebase
createdAt: 'created_at'
},
as: 'object'
})
// Use camelCase locally
posts$['post-1'].userId.set('user-123')
// Saved as snake_case in Firebase
// { user_id: 'user-123' }
Soft Deletes
const posts$ = syncedFirebase({
refPath: () => 'posts',
fieldDeleted: 'deleted',
as: 'object'
})
// Delete sets deleted: true
posts$['post-1'].delete()
// In Firebase: { id: 'post-1', deleted: true }
Read-Only
const leaderboard$ = syncedFirebase({
refPath: () => 'leaderboard',
readonly: true, // No writes
as: 'array'
})
// Can't modify
// leaderboard$[0].score.set(100) // No-op or error
With Persistence
import { ObservablePersistIndexedDB } from '@legendapp/state/persist-plugins/indexeddb'
const posts$ = syncedFirebase({
refPath: () => 'posts',
persist: {
name: 'posts',
plugin: ObservablePersistIndexedDB
}
})
// 1. Loads from IndexedDB (instant)
// 2. Syncs with Firebase in background
// 3. Subscribes to real-time updates
// 4. Saves changes to both IndexedDB and Firebase
Multi-Tenant
const tenantId$ = observable('tenant-123')
const data$ = syncedFirebase({
refPath: () => `tenants/${tenantId$.get()}/data`,
persist: {
name: computed(() => `data-${tenantId$.get()}`)
}
})
// Switch tenant - automatically refetches
tenantId$.set('tenant-456')
Error Handling
const posts$ = syncedFirebase({
refPath: () => 'posts',
onError: (error, { source, type, retry }) => {
console.error(`Firebase ${source} error:`, error)
if (error.message.includes('permission-denied')) {
showNotification('You do not have permission')
} else if (error.message.includes('network')) {
// Will retry automatically
} else {
showNotification(`Failed to ${source}`)
}
}
})
Offline Support
import { getDatabase, enablePersistence } from 'firebase/database'
// Enable Firebase offline persistence
const db = getDatabase()
enablePersistence(db)
const posts$ = syncedFirebase({
refPath: () => 'posts',
persist: {
name: 'posts',
plugin: ObservablePersistIndexedDB,
retrySync: true
}
})
// Works offline with both Firebase and Legend-State caching
Real-time Subscriptions
Firebase real-time automatically:
- Subscribes using
onChildAdded, onChildChanged, onChildRemoved
- Updates observable when data changes
- Handles concurrent edits with Firebase’s conflict resolution
- Unsubscribes when observable is no longer observed
const messages$ = syncedFirebase({
refPath: () => 'chat/messages',
realtime: true,
as: 'array'
})
// Real-time updates are automatic
// No manual subscription management needed
Write Lifecycle
Firebase writes go through a careful lifecycle:
- Local update: Change applied to observable immediately
- Optimistic UI: User sees change right away
- Firebase write: Change sent to Firebase
- Server confirmation: Firebase confirms write
- Server value: Firebase sends back server value (e.g., timestamps)
- Final update: Observable updated with server value
This ensures:
- Fast UI updates
- Conflict resolution
- Server-generated values (timestamps, IDs)
Security Rules
Firebase security rules are automatically respected:
// Firebase rules
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
}
}
}
// Observable automatically respects rules
const myData$ = syncedFirebase({
refPath: (uid) => `users/${uid}/data`
})
// Can only access own data (enforced by rules)
Best Practices
- Use security rules: Always enforce access control in Firebase rules
- Enable offline persistence: Use Firebase offline mode + Legend-State caching
- Use fieldTransforms: Keep local code clean with camelCase
- Handle auth: Use
requireAuth: true for user-specific data
- Query efficiently: Use Firebase queries to limit data transfer
- Monitor writes: Use
onError to handle write failures
Migration from Direct Firebase
// Old: Direct Firebase
import { ref, onValue, set } from 'firebase/database'
const postsRef = ref(db, 'posts')
onValue(postsRef, (snapshot) => {
const data = snapshot.val()
// Manual state management
})
set(postsRef, newData)
// New: syncedFirebase
const posts$ = syncedFirebase({
refPath: () => 'posts'
})
// Automatic state management
// Real-time updates built-in
// Local caching available
See Also