TS
AdvancedFirebase

Firebase Firestore Real-time Hook

A React hook for real-time Firestore data with automatic subscription management and error handling.

ReactFirebaseFirestoreReal-timeTypeScript
TypeScript
153 lines
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>
  );
}
*/

Performance

Optimized with automatic cleanup and connection pooling

Best Practices

  • Automatic subscription cleanup
  • Generic type support
  • Error boundary integration
  • Loading state management
February 25, 2024
GitHub