A React hook for real-time Firestore data with automatic subscription management and error handling.
import { useState, useEffect, useRef } from 'react';
import {
collection,
doc,
onSnapshot,
query,
Query,
DocumentReference,
FirestoreError
} from 'firebase/firestore';
import { db } from '../lib/firebase';
interface UseFirestoreState<T> {
data: T | null;
loading: boolean;
error: FirestoreError | null;
}
// Hook for single document
export function useDocument<T = any>(
path: string | null
): UseFirestoreState<T> {
const [state, setState] = useState<UseFirestoreState<T>>({
data: null,
loading: true,
error: null
});
const unsubscribeRef = useRef<(() => void) | null>(null);
useEffect(() => {
if (!path) {
setState({ data: null, loading: false, error: null });
return;
}
setState(prev => ({ ...prev, loading: true, error: null }));
const docRef = doc(db, path) as DocumentReference<T>;
unsubscribeRef.current = onSnapshot(
docRef,
(snapshot) => {
if (snapshot.exists()) {
setState({
data: { id: snapshot.id, ...snapshot.data() } as T,
loading: false,
error: null
});
} else {
setState({
data: null,
loading: false,
error: null
});
}
},
(error) => {
setState({
data: null,
loading: false,
error
});
}
);
return () => {
if (unsubscribeRef.current) {
unsubscribeRef.current();
unsubscribeRef.current = null;
}
};
}, [path]);
return state;
}
// Hook for collection queries
export function useCollection<T = any>(
collectionPath: string | null,
queryConstraints?: Query<T>
): UseFirestoreState<T[]> {
const [state, setState] = useState<UseFirestoreState<T[]>>({
data: null,
loading: true,
error: null
});
const unsubscribeRef = useRef<(() => void) | null>(null);
useEffect(() => {
if (!collectionPath) {
setState({ data: null, loading: false, error: null });
return;
}
setState(prev => ({ ...prev, loading: true, error: null }));
const collectionRef = collection(db, collectionPath);
const queryRef = queryConstraints || collectionRef;
unsubscribeRef.current = onSnapshot(
queryRef as Query<T>,
(snapshot) => {
const data = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
})) as T[];
setState({
data,
loading: false,
error: null
});
},
(error) => {
setState({
data: null,
loading: false,
error
});
}
);
return () => {
if (unsubscribeRef.current) {
unsubscribeRef.current();
unsubscribeRef.current = null;
}
};
}, [collectionPath, queryConstraints]);
return state;
}
// Usage example:
/*
function PostsList() {
const { data: posts, loading, error } = useCollection<Post>('posts');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!posts) return <div>No posts found</div>;
return (
<div>
{posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
*/
Optimized with automatic cleanup and connection pooling
A reusable custom hook that manages Firebase authentication state with loading states and error hand...
A robust Next.js API route with comprehensive error handling, validation, and response formatting.