Collection
Collection Service
Advanced Firestore collection management with real-time updates, querying, and reactive state using Svelte 5 runes
Collection Service
The firekitCollection
service provides advanced Firestore collection management with real-time updates, complex querying, pagination, caching, and reactive state using Svelte 5 runes.
Overview
The collection service offers:
- Real-time collection subscriptions with automatic updates
- Advanced query building with type safety
- Pagination and infinite scrolling support
- Intelligent caching with TTL
- Performance monitoring and statistics
- Collection groups and subcollections
- Reactive state management with Svelte 5 runes
Basic Usage
import { firekitCollection, where, orderBy, limit } from 'svelte-firekit';
// Simple collection subscription
const users = firekitCollection<User>('users');
// With query constraints
const activeUsers = firekitCollection<User>(
'users',
where('active', '==', true),
orderBy('name'),
limit(10)
);
Reactive State Management
Basic Reactive State
import { firekitCollection } from 'svelte-firekit';
// Create collection subscription
const posts = firekitCollection<Post>('posts');
// Access reactive state using Svelte 5 runes
const postsData = $derived(posts.data);
const postsLoading = $derived(posts.loading);
const postsError = $derived(posts.error);
const postsEmpty = $derived(posts.empty);
const postsSize = $derived(posts.size);
// React to state changes
$effect(() => {
if (postsLoading) {
console.log('Loading posts...');
} else if (postsError) {
console.error('Posts error:', postsError);
} else {
console.log('Posts loaded:', postsData.length);
}
});
Advanced Reactive State
import { firekitCollection } from 'svelte-firekit';
// Collection with advanced options
const users = firekitCollection<User>('users', {
pagination: { enabled: true, pageSize: 20 },
cache: { enabled: true, ttl: 300000 },
transform: (doc) => ({ ...doc, displayName: doc.name.toUpperCase() })
});
// Access all reactive properties
const usersData = $derived(users.data);
const usersLoading = $derived(users.loading);
const usersInitialized = $derived(users.initialized);
const usersError = $derived(users.error);
const usersEmpty = $derived(users.empty);
const usersSize = $derived(users.size);
const usersLastUpdated = $derived(users.lastUpdated);
const usersState = $derived(users.state);
// React to collection changes
$effect(() => {
console.log('Collection state:', {
loading: usersLoading,
initialized: usersInitialized,
error: usersError,
dataCount: usersData.length,
lastUpdated: usersLastUpdated
});
});
Query Building
Basic Queries
import { firekitCollection, where, orderBy, limit } from 'svelte-firekit';
// Simple where clause
const activeUsers = firekitCollection<User>('users', where('active', '==', true));
// Multiple conditions
const premiumUsers = firekitCollection<User>(
'users',
where('active', '==', true),
where('subscription', '==', 'premium'),
orderBy('createdAt', 'desc')
);
// With limit
const recentPosts = firekitCollection<Post>(
'posts',
where('published', '==', true),
orderBy('createdAt', 'desc'),
limit(10)
);
Advanced Queries
import { firekitCollection, where, orderBy, limit, startAt, endAt } from 'svelte-firekit';
// Complex query with multiple conditions
const filteredPosts = firekitCollection<Post>(
'posts',
where('category', 'in', ['tech', 'design', 'business']),
where('published', '==', true),
where('authorId', '==', currentUserId),
orderBy('createdAt', 'desc'),
orderBy('title'),
limit(20)
);
// Query with date range
const recentPosts = firekitCollection<Post>(
'posts',
where('createdAt', '>=', new Date('2024-01-01')),
where('createdAt', '<=', new Date('2024-12-31')),
orderBy('createdAt', 'desc')
);
// Query with array contains
const taggedPosts = firekitCollection<Post>(
'posts',
where('tags', 'array-contains', 'javascript'),
orderBy('createdAt', 'desc')
);
Query Builder Pattern
import { firekitCollection } from 'svelte-firekit';
// Create query builder
const query = firekitCollection
.createQuery()
.where('status', '==', 'active')
.where('category', 'in', ['tech', 'design'])
.orderBy('createdAt', 'desc')
.limit(20);
// Use the query
const results = firekitCollection<Post>('posts', query.build());
// Chain queries
const techPosts = firekitCollection
.createQuery()
.where('category', '==', 'tech')
.orderBy('createdAt', 'desc')
.limit(10);
const designPosts = firekitCollection
.createQuery()
.where('category', '==', 'design')
.orderBy('createdAt', 'desc')
.limit(10);
// Use both queries
const techResults = firekitCollection<Post>('posts', techPosts.build());
const designResults = firekitCollection<Post>('posts', designPosts.build());
Pagination
Basic Pagination
import { firekitCollection } from 'svelte-firekit';
// Collection with pagination enabled
const paginatedUsers = firekitCollection<User>('users', {
pagination: {
enabled: true,
pageSize: 20
}
});
// Access pagination methods
const currentPage = $derived(paginatedUsers.currentPage);
const totalPages = $derived(paginatedUsers.totalPages);
const hasNextPage = $derived(paginatedUsers.hasNextPage);
const hasPreviousPage = $derived(paginatedUsers.hasPreviousPage);
// Pagination functions
async function loadNextPage() {
await paginatedUsers.nextPage();
}
async function loadPreviousPage() {
await paginatedUsers.previousPage();
}
async function goToPage(page: number) {
await paginatedUsers.goToPage(page);
}
Infinite Scrolling
import { firekitCollection } from 'svelte-firekit';
// Collection with infinite scrolling
const infinitePosts = firekitCollection<Post>('posts', {
pagination: {
enabled: true,
pageSize: 10,
mode: 'infinite'
},
orderBy: 'createdAt',
direction: 'desc'
});
// Load more data
async function loadMorePosts() {
if (infinitePosts.hasNextPage) {
await infinitePosts.loadMore();
}
}
// React to data changes
$effect(() => {
console.log('Total posts loaded:', infinitePosts.data.length);
console.log('Has more posts:', infinitePosts.hasNextPage);
});
Caching
Basic Caching
import { firekitCollection } from 'svelte-firekit';
// Collection with caching
const cachedUsers = firekitCollection<User>('users', {
cache: {
enabled: true,
ttl: 300000 // 5 minutes
}
});
// Access cache statistics
const cacheStats = $derived(cachedUsers.getStats());
console.log('Cache hit rate:', cacheStats.cacheHitRate);
Advanced Caching
import { firekitCollection } from 'svelte-firekit';
// Collection with advanced caching
const advancedCachedPosts = firekitCollection<Post>('posts', {
cache: {
enabled: true,
ttl: 600000, // 10 minutes
maxSize: 1000, // Maximum cache entries
strategy: 'lru' // Least recently used eviction
},
transform: (doc) => ({
...doc,
cachedAt: new Date(),
displayTitle: doc.title.toUpperCase()
})
});
// Clear cache
function clearCache() {
advancedCachedPosts.clearCache();
}
// Check cache status
function checkCacheStatus() {
const stats = advancedCachedPosts.getStats();
console.log('Cache statistics:', stats);
}
Data Transformation
Basic Transformation
import { firekitCollection } from 'svelte-firekit';
// Transform data on load
const transformedUsers = firekitCollection<User>('users', {
transform: (user) => ({
...user,
displayName: user.name.toUpperCase(),
fullName: `${user.firstName} ${user.lastName}`,
age: calculateAge(user.birthDate)
})
});
Advanced Transformation
import { firekitCollection } from 'svelte-firekit';
// Complex transformation with validation
const validatedPosts = firekitCollection<Post>('posts', {
transform: (post) => {
// Validate required fields
if (!post.title || !post.content) {
console.warn('Invalid post:', post.id);
return null; // Filter out invalid posts
}
// Transform and enrich data
return {
...post,
title: post.title.trim(),
content: post.content.trim(),
wordCount: post.content.split(' ').length,
readingTime: Math.ceil(post.content.split(' ').length / 200), // 200 words per minute
excerpt: post.content.substring(0, 150) + '...',
formattedDate: new Date(post.createdAt).toLocaleDateString(),
isRecent: new Date(post.createdAt) > new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
};
},
filter: (post) => post !== null // Remove null values
});
Collection Groups
Basic Collection Groups
import { firekitCollectionGroup } from 'svelte-firekit';
// Query all 'comments' subcollections across all documents
const allComments = firekitCollectionGroup<Comment>('comments', {
orderBy: 'createdAt',
direction: 'desc'
});
// Filter comments by author
const userComments = firekitCollectionGroup<Comment>('comments', {
where: 'authorId',
operator: '==',
value: currentUserId,
orderBy: 'createdAt',
direction: 'desc'
});
Advanced Collection Groups
import { firekitCollectionGroup } from 'svelte-firekit';
// Complex collection group query
const recentComments = firekitCollectionGroup<Comment>('comments', {
where: 'createdAt',
operator: '>=',
value: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // Last 30 days
where: 'approved',
operator: '==',
value: true,
orderBy: 'createdAt',
direction: 'desc',
limit: 50
});
Data Manipulation
Filtering Data
import { firekitCollection } from 'svelte-firekit';
const posts = firekitCollection<Post>('posts');
// Filter data client-side
const publishedPosts = $derived(posts.filter((post) => post.published));
const recentPosts = $derived(
posts.filter((post) => new Date(post.createdAt) > new Date(Date.now() - 7 * 24 * 60 * 60 * 1000))
);
// Find specific item
const specificPost = $derived(posts.find((post) => post.id === 'post-123'));
// Find by ID
const postById = $derived(posts.findById('post-123'));
Sorting and Grouping
import { firekitCollection } from 'svelte-firekit';
const users = firekitCollection<User>('users');
// Sort data
const sortedUsers = $derived(users.sort((a, b) => a.name.localeCompare(b.name)));
const recentUsers = $derived(
users.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
);
// Group by field
const usersByRole = $derived(users.groupBy('role'));
const postsByCategory = $derived(posts.groupBy('category'));
// Get unique values
const uniqueCategories = $derived(posts.unique('category'));
const uniqueRoles = $derived(users.unique('role'));
Counting and Aggregation
import { firekitCollection } from 'svelte-firekit';
const posts = firekitCollection<Post>('posts');
// Count items
const totalPosts = $derived(posts.count());
const publishedPostsCount = $derived(posts.count((post) => post.published));
const recentPostsCount = $derived(
posts.count((post) => new Date(post.createdAt) > new Date(Date.now() - 7 * 24 * 60 * 60 * 1000))
);
// Check conditions
const hasPublishedPosts = $derived(posts.some((post) => post.published));
const allPostsPublished = $derived(posts.every((post) => post.published));
Performance Monitoring
Collection Statistics
import { firekitCollection } from 'svelte-firekit';
const users = firekitCollection<User>('users');
// Get performance statistics
const stats = $derived(users.getStats());
$effect(() => {
console.log('Collection Statistics:', {
totalDocuments: stats.totalDocuments,
readCount: stats.readCount,
writeCount: stats.writeCount,
cacheHitRate: stats.cacheHitRate,
averageQueryTime: stats.averageQueryTime,
memoryUsage: stats.memoryUsage,
lastActivity: stats.lastActivity
});
});
Reset Statistics
import { firekitCollection } from 'svelte-firekit';
const posts = firekitCollection<Post>('posts');
// Reset performance statistics
function resetStats() {
posts.resetStats();
console.log('Statistics reset');
}
Error Handling
Basic Error Handling
import { firekitCollection } from 'svelte-firekit';
const posts = firekitCollection<Post>('posts');
// React to errors
$effect(() => {
if (posts.error) {
console.error('Collection error:', posts.error);
if (posts.error.isRetryable()) {
// Retry the operation
posts.retryIfNeeded();
}
}
});
Advanced Error Handling
import { firekitCollection } from 'svelte-firekit';
const users = firekitCollection<User>('users');
// Handle different error types
$effect(() => {
if (users.error) {
switch (users.error.code) {
case 'permission-denied':
console.error('Permission denied - check security rules');
break;
case 'not-found':
console.error('Collection not found');
break;
case 'unavailable':
console.error('Service unavailable - retrying...');
users.retryIfNeeded();
break;
default:
console.error('Unknown error:', users.error);
}
}
});
Svelte Component Integration
Basic Collection Component
<script lang="ts">
import { firekitCollection, where, orderBy, limit } from 'svelte-firekit';
interface Post {
id: string;
title: string;
content: string;
author: string;
createdAt: Date;
published: boolean;
}
// Create collection subscription
const posts = firekitCollection<Post>(
'posts',
where('published', '==', true),
orderBy('createdAt', 'desc'),
limit(10)
);
// Reactive state
const postsData = $derived(posts.data);
const postsLoading = $derived(posts.loading);
const postsError = $derived(posts.error);
const postsEmpty = $derived(posts.empty);
</script>
{#if postsLoading}
<div class="loading">
<p>Loading posts...</p>
</div>
{:else if postsError}
<div class="error">
<p>Error loading posts: {postsError.message}</p>
<button onclick={() => posts.retryIfNeeded()}>Retry</button>
</div>
{:else if postsEmpty}
<div class="empty">
<p>No posts found</p>
</div>
{:else}
<div class="posts">
{#each postsData as post (post.id)}
<article class="post">
<h2>{post.title}</h2>
<p class="author">By {post.author}</p>
<p class="content">{post.content}</p>
<p class="date">
{new Date(post.createdAt).toLocaleDateString()}
</p>
</article>
{/each}
</div>
{/if}
<style>
.loading,
.error,
.empty {
text-align: center;
padding: 2rem;
}
.error {
color: #dc3545;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 8px;
}
.posts {
display: grid;
gap: 1.5rem;
}
.post {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.post h2 {
margin: 0 0 0.5rem 0;
color: #333;
}
.author {
color: #666;
font-size: 0.9rem;
margin: 0 0 1rem 0;
}
.content {
color: #444;
line-height: 1.6;
margin: 0 0 1rem 0;
}
.date {
color: #999;
font-size: 0.8rem;
margin: 0;
}
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
background: #007bff;
color: white;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
</style>
Advanced Collection Component with Pagination
<script lang="ts">
import { firekitCollection, where, orderBy } from 'svelte-firekit';
interface User {
id: string;
name: string;
email: string;
role: string;
active: boolean;
createdAt: Date;
}
// Collection with pagination
const users = firekitCollection<User>('users', {
pagination: {
enabled: true,
pageSize: 20
},
where: 'active',
operator: '==',
value: true,
orderBy: 'createdAt',
direction: 'desc'
});
// Reactive state
const usersData = $derived(users.data);
const usersLoading = $derived(users.loading);
const usersError = $derived(users.error);
const currentPage = $derived(users.currentPage);
const totalPages = $derived(users.totalPages);
const hasNextPage = $derived(users.hasNextPage);
const hasPreviousPage = $derived(users.hasPreviousPage);
// Pagination functions
async function loadNextPage() {
if (hasNextPage) {
await users.nextPage();
}
}
async function loadPreviousPage() {
if (hasPreviousPage) {
await users.previousPage();
}
}
async function goToPage(page: number) {
await users.goToPage(page);
}
// Search functionality
let searchTerm = '';
let filteredUsers = $derived(
usersData.filter(
(user) =>
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase())
)
);
</script>
<div class="users-dashboard">
<header class="dashboard-header">
<h1>Users Dashboard</h1>
<div class="search">
<input type="text" placeholder="Search users..." bindvalue={searchTerm} />
</div>
</header>
{#if usersLoading}
<div class="loading">
<p>Loading users...</p>
</div>
{:else if usersError}
<div class="error">
<p>Error: {usersError.message}</p>
<button onclick={() => users.retryIfNeeded()}>Retry</button>
</div>
{:else}
<main class="dashboard-content">
<div class="users-table">
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each filteredUsers as user (user.id)}
<tr>
<td>{user.name}</td>
<td>{user.email}</td>
<td>
<span class="role-badge role-{user.role}">
{user.role}
</span>
</td>
<td>
{new Date(user.createdAt).toLocaleDateString()}
</td>
<td>
<button class="btn-small">Edit</button>
<button class="btn-small btn-danger">Delete</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<div class="pagination">
<button disabled={!hasPreviousPage} onclick={loadPreviousPage}> Previous </button>
<span class="page-info">
Page {currentPage} of {totalPages}
</span>
<button disabled={!hasNextPage} onclick={loadNextPage}> Next </button>
</div>
<div class="stats">
<p>Showing {filteredUsers.length} of {usersData.length} users</p>
</div>
</main>
{/if}
</div>
<style>
.users-dashboard {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.search input {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
width: 300px;
}
.loading,
.error {
text-align: center;
padding: 2rem;
}
.error {
color: #dc3545;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 8px;
}
.users-table {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 1rem;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #333;
}
.role-badge {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
.role-admin {
background: #dc3545;
color: white;
}
.role-user {
background: #28a745;
color: white;
}
.role-moderator {
background: #ffc107;
color: #333;
}
.btn-small {
padding: 0.25rem 0.5rem;
border: none;
border-radius: 4px;
background: #007bff;
color: white;
cursor: pointer;
font-size: 0.8rem;
margin-right: 0.5rem;
}
.btn-danger {
background: #dc3545;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin: 2rem 0;
}
.page-info {
font-weight: 500;
color: #666;
}
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
background: #007bff;
color: white;
cursor: pointer;
}
button:hover:not(:disabled) {
background: #0056b3;
}
button:disabled {
background: #6c757d;
cursor: not-allowed;
}
.stats {
text-align: center;
color: #666;
font-size: 0.9rem;
}
</style>
Type Definitions
Collection Options
interface CollectionOptions {
pagination?: {
enabled: boolean;
pageSize?: number;
mode?: 'paged' | 'infinite';
};
cache?: {
enabled: boolean;
ttl?: number;
maxSize?: number;
strategy?: 'lru' | 'fifo';
};
transform?: (doc: T) => T | null;
filter?: (doc: T) => boolean;
orderBy?: string;
direction?: 'asc' | 'desc';
where?: string;
operator?: FirestoreOperator;
value?: any;
}
Collection State
interface CollectionState<T> {
data: T[];
loading: boolean;
initialized: boolean;
error: CollectionError | null;
empty: boolean;
size: number;
lastUpdated: Date | null;
}
Collection Statistics
interface CollectionStats {
totalDocuments: number;
readCount: number;
writeCount: number;
cacheHitRate: number;
averageQueryTime: number;
lastActivity: Date;
memoryUsage: number;
}
Best Practices
1. Use Type Safety
// ✅ Good - Define interfaces
interface User {
id: string;
name: string;
email: string;
active: boolean;
}
const users = firekitCollection<User>('users');
// ❌ Avoid - Using any
const users = firekitCollection('users');
2. Optimize Queries
// ✅ Good - Specific queries with limits
const recentPosts = firekitCollection<Post>(
'posts',
where('published', '==', true),
orderBy('createdAt', 'desc'),
limit(20)
);
// ❌ Avoid - Fetching all data
const allPosts = firekitCollection<Post>('posts');
3. Handle Loading States
{#if collection.loading}
<LoadingSpinner />
{:else if collection.error}
<ErrorMessage error={collection.error} />
{:else if collection.empty}
<EmptyState />
{:else}
<DataList data={collection.data} />
{/if}
4. Use Caching Appropriately
// For frequently accessed, rarely changed data
const userProfiles = firekitCollection<User>('users', {
cache: { enabled: true, ttl: 300000 } // 5 minutes
});
// For real-time data, disable caching
const chatMessages = firekitCollection<Message>('messages', {
cache: { enabled: false }
});
5. Clean Up Resources
import { onDestroy } from 'svelte';
const posts = firekitCollection<Post>('posts');
onDestroy(() => {
posts.dispose();
});
API Reference
Properties
data
- Collection data (reactive)loading
- Loading state (reactive)initialized
- Initialization state (reactive)error
- Current error (reactive)empty
- Empty state (reactive)size
- Collection size (reactive)lastUpdated
- Last update timestamp (reactive)state
- Complete state object (reactive)ref
- Firestore collection referencequeryReference
- Firestore query referencepath
- Collection path
Methods
refresh()
- Refresh collection datagetFromServer()
- Fetch data from serveraddConstraints(...constraints)
- Add query constraintscreateQuery()
- Create query builderwithQuery(builder)
- Use query builderfilter(predicate)
- Filter data client-sidefind(predicate)
- Find specific itemfindById(id)
- Find item by IDsort(compareFn)
- Sort datapaginate(page, pageSize)
- Paginate datagroupBy(field)
- Group data by fieldunique(field)
- Get unique valuescount(predicate?)
- Count itemssome(predicate)
- Check if some items matchevery(predicate)
- Check if all items matchsetRealtimeMode(realtime)
- Toggle real-time modeclearCache()
- Clear cachegetStats()
- Get performance statisticsresetStats()
- Reset statisticswaitForInitialization()
- Wait for initializationdispose()
- Clean up resources
Pagination Methods
nextPage()
- Load next pagepreviousPage()
- Load previous pagegoToPage(page)
- Go to specific pageloadMore()
- Load more data (infinite scroll)
Next Steps
- Document Service - Firestore document management
- Mutations Service - Document mutations and batch operations
- Storage Service - File upload/download
- Presence Service - User online/offline tracking
- Analytics Service - Event tracking