Package Exports
- @valian/rxjs-firebase
Readme
@valian/rxjs-firebase
RxJS operators and utilities for Firebase with real-time updates and TypeScript support
Description
@valian/rxjs-firebase is a lightweight RxJS library that provides seamless integration with Firebase services. It offers real-time data synchronization, TypeScript support, and comprehensive state management for your RxJS-based applications.
Key Features
- 🔄 Real-time updates - Automatic synchronization with Firebase using
onSnapshot - 📘 Full TypeScript support - Type-safe operators and utilities with generic type parameters
- 🎯 Simple API - Easy-to-use operators that handle loading, error, and data states
- ⚡ Lightweight - Minimal bundle size with zero dependencies
- 🛡️ Error handling - Built-in error management with optional custom error handlers
- 🔧 Flexible - Works with any Firebase query, document reference, or auth state
- 📊 State management - Comprehensive state objects with loading, error, and data states
Getting Started
Installation
pnpm add @valian/rxjs-firebasePrerequisites
This library requires the following peer dependencies:
rxjs^7 || ^8firebase^11 || ^12
Setup
Make sure you have Firebase initialized in your project:
import { initializeApp } from 'firebase/app'
import { getFirestore } from 'firebase/firestore'
const firebaseConfig = {
// your config
}
const app = initializeApp(firebaseConfig)
export const db = getFirestore(app)Usage Examples
queryState Operator
The queryState operator transforms a QuerySnapshot observable into a QuerySnapshotState observable with loading, error, and data states.
Basic Usage
import { fromQuery, queryState } from '@valian/rxjs-firebase'
import { collection, query, where } from 'firebase/firestore'
import { db } from './firebase'
const todosQuery = query(collection(db, 'todos'), where('completed', '==', false))
const todos$ = fromQuery(todosQuery).pipe(queryState())
todos$.subscribe((state) => {
if (state.isLoading) {
console.log('Loading todos...')
} else if (state.hasError) {
console.log('Error loading todos')
} else {
console.log('Todos:', state.data)
console.log('Count:', state.size)
}
})With TypeScript
interface Todo {
id: string
title: string
completed: boolean
createdAt: Date
}
const todosQuery = query(collection(db, 'todos'))
const todos$ = fromQuery<Todo>(todosQuery).pipe(queryState<Todo>())
todos$.subscribe((state) => {
if (state.isLoading) {
console.log('Loading...')
} else if (state.hasError) {
console.log('Error occurred')
} else if (state.empty) {
console.log('No todos found')
} else {
console.log(`Found ${state.size} todos:`)
state.data.forEach((todo) => {
console.log(`- ${todo.title} (${todo.completed ? '✓' : '○'})`)
})
}
})With Error Handling
const todos$ = fromQuery(todosQuery).pipe(
queryState({
onSnapshot: (state) => console.log('State updated:', state),
onError: (error) => {
console.error('Firestore error:', error)
// Handle error appropriately
},
}),
)documentState Operator
The documentState operator transforms a DocumentSnapshot observable into a DocumentSnapshotState observable with loading, error, and data states.
Basic Usage
import { fromDocumentRef, documentState } from '@valian/rxjs-firebase'
import { doc } from 'firebase/firestore'
import { db } from './firebase'
function getUserProfile(userId: string) {
const userRef = doc(db, 'users', userId)
const user$ = fromDocumentRef(userRef).pipe(documentState())
return user$.subscribe((state) => {
if (state.isLoading) {
console.log('Loading user...')
} else if (state.hasError) {
console.log('Error loading user')
} else if (!state.exists) {
console.log('User not found')
} else {
console.log('User data:', state.data)
}
})
}With TypeScript
interface User {
name: string
email: string
avatar?: string
createdAt: Date
}
function getTypedUserProfile(userId: string) {
const userRef = doc(db, 'users', userId)
const user$ = fromDocumentRef<User>(userRef).pipe(documentState<User>())
return user$.subscribe((state) => {
if (state.isLoading) {
console.log('Loading...')
} else if (state.hasError) {
console.log('Error loading user')
} else if (!state.exists) {
console.log('User not found')
} else {
const user = state.data
console.log(`User: ${user.name} (${user.email})`)
if (user.avatar) {
console.log(`Avatar: ${user.avatar}`)
}
}
})
}Source Functions
fromQuery
Creates an observable from a Firestore query.
import { fromQuery } from '@valian/rxjs-firebase'
import { collection, query, where, orderBy } from '@firebase/firestore'
// Basic collection
const todos$ = fromQuery(collection(db, 'todos'))
// With filters and ordering
const activeTodos$ = fromQuery(
query(collection(db, 'todos'), where('completed', '==', false), orderBy('createdAt', 'desc')),
)fromDocumentRef
Creates an observable from a Firestore document reference.
import { fromDocumentRef } from '@valian/rxjs-firebase'
import { doc } from 'firebase/firestore'
const user$ = fromDocumentRef(doc(db, 'users', 'user123'))authState
Creates an observable from Firebase Auth state changes.
import { authState } from '@valian/rxjs-firebase'
const auth$ = authState()
auth$.subscribe((user) => {
if (user) {
console.log('User signed in:', user.uid)
} else {
console.log('User signed out')
}
})Subjects
DocumentSnapshotSubject
A BehaviorSubject that manages document snapshot state with loading, error, and data states. Provides convenient methods for working with document data.
import { fromDocumentRef, DocumentSnapshotSubject } from '@valian/rxjs-firebase'
import { doc } from 'firebase/firestore'
const userRef = doc(db, 'users', 'user123')
const doc$ = fromDocumentRef(userRef)
const subject = new DocumentSnapshotSubject(doc$, {
onSnapshot: (state) => console.log('State updated:', state),
onError: (error) => console.error('Error:', error),
})
// Subscribe to state changes
subject.subscribe((state) => {
if (state.isLoading) {
console.log('Loading...')
} else if (state.hasError) {
console.log('Error occurred')
} else if (state.exists) {
console.log('Document data:', state.data)
} else {
console.log('Document does not exist')
}
})
// Access current data directly
const currentData = subject.data
// Wait for document to exist (with timeout)
const exists = await subject.exists(5000) // 5 second timeout
// Clean up when done
subject.close()QuerySnapshotSubject
A BehaviorSubject that manages query snapshot state with loading, error, and data states. Provides convenient methods for working with query results.
import { fromQuery, QuerySnapshotSubject } from '@valian/rxjs-firebase'
import { collection } from 'firebase/firestore'
const todos$ = fromQuery(collection(db, 'todos'))
const subject = new QuerySnapshotSubject(todos$, {
onSnapshot: (state) => console.log('State updated:', state),
onError: (error) => console.error('Error:', error),
})
// Subscribe to state changes
subject.subscribe((state) => {
if (state.isLoading) {
console.log('Loading...')
} else if (state.hasError) {
console.log('Error occurred')
} else {
console.log('Query results:', state.data)
console.log('Count:', state.size)
}
})
// Access current data directly
const currentData = subject.data
// Clean up when done
subject.close()API Reference
queryState
const state$ = query$.pipe(
queryState<AppModelType, DbModelType>({
onSnapshot?: (state: QuerySnapshotState<AppModelType>) => void,
onError?: (error: unknown) => void
})
)Parameters
onSnapshot: Optional callback function called when state updatesonError: Optional error handler function
Returns
An observable that emits QuerySnapshotState<AppModelType> objects with:
{
data: AppModelType[] // Array of documents
snapshot?: QuerySnapshot // Firestore snapshot
isLoading: boolean // True while loading
isDisabled: boolean // True when query is null
hasError: boolean // True if error occurred
empty: boolean // True if collection is empty
size: number // Number of documents
}documentState
const state$ = doc$.pipe(
documentState<AppModelType, DbModelType>({
onSnapshot?: (state: DocumentSnapshotState<AppModelType>) => void,
onError?: (error: unknown) => void
})
)Parameters
onSnapshot: Optional callback function called when state updatesonError: Optional error handler function
Returns
An observable that emits DocumentSnapshotState<AppModelType> objects with:
{
data?: AppModelType // Document data (undefined if loading/error/not exists)
snapshot?: DocumentSnapshot // Firestore snapshot
isLoading: boolean // True while loading
isDisabled: boolean // True when ref is null/undefined
hasError: boolean // True if error occurred
exists?: boolean // True if document exists
}fromQuery
const query$ = fromQuery<AppModelType, DbModelType>(
query: Query<AppModelType, DbModelType>
)Creates an observable that emits QuerySnapshot objects when the query results change.
fromDocumentRef
const doc$ = fromDocumentRef<AppModelType, DbModelType>(
ref: DocumentReference<AppModelType, DbModelType>
)Creates an observable that emits DocumentSnapshot objects when the document changes.
authState
const auth$ = authState()Creates an observable that emits the current user or null when authentication state changes.
DocumentSnapshotSubject
class DocumentSnapshotSubject<AppModelType> extends BehaviorSubject<DocumentSnapshotState<AppModelType>>Methods
data: Getter that returns the current document dataexists(timeout?: number): Returns a Promise that resolves totrueif the document exists,falseotherwise. Default timeout is 10 seconds.close(): Unsubscribes from the underlying observable and completes the subject
QuerySnapshotSubject
class QuerySnapshotSubject<AppModelType> extends BehaviorSubject<QuerySnapshotState<AppModelType>>Methods
data: Getter that returns the current query results as an arrayclose(): Unsubscribes from the underlying observable and completes the subject
Advanced Usage
Dynamic Queries
import { combineLatest, switchMap } from 'rxjs'
import { fromQuery, queryState } from '@valian/rxjs-firebase'
function createFilteredTodos(userId$: Observable<string>, completed$: Observable<boolean>) {
return combineLatest([userId$, completed$]).pipe(
switchMap(([userId, completed]) => {
const query = query(collection(db, 'todos'), where('userId', '==', userId), where('completed', '==', completed))
return fromQuery(query).pipe(queryState())
}),
)
}Combining Multiple Observables
import { combineLatest } from 'rxjs'
import { map } from 'rxjs/operators'
import { fromDocumentRef, fromQuery, documentState, queryState } from '@valian/rxjs-firebase'
function getUserWithTodos(userId: string) {
const user$ = fromDocumentRef(doc(db, 'users', userId)).pipe(documentState<User>())
const todos$ = fromQuery(query(collection(db, 'todos'), where('userId', '==', userId))).pipe(queryState<Todo>())
return combineLatest([user$, todos$]).pipe(
map(([userState, todosState]) => ({
user: userState.data,
todos: todosState.data,
isLoading: userState.isLoading || todosState.isLoading,
hasError: userState.hasError || todosState.hasError,
})),
)
}License
MIT © Valian
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Support
If you encounter any issues or have questions, please open an issue on GitHub.