M2: refactor hooks to storage adapter (subscribe)
- src/storage/index.js: getStorage() factory + SDK re-exports - App.js: useFirestoreDocument/Collection call storage.subscribeDoc/Collection - getStorage import added 56 frontend tests green. Hooks now impl-agnostic (firebase vs ws).
This commit is contained in:
+18
-39
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||||
import { initializeApp } from './storage';
|
import { initializeApp } from './storage';
|
||||||
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from './storage';
|
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from './storage';
|
||||||
import { getFirestore, doc, setDoc, getDoc, getDocs, addDoc, collection, onSnapshot, updateDoc, deleteDoc, query, orderBy, limit, writeBatch } from './storage';
|
import { getFirestore, doc, setDoc, getDoc, getDocs, addDoc, collection, onSnapshot, updateDoc, deleteDoc, query, orderBy, limit, writeBatch, getStorage } from './storage';
|
||||||
import {
|
import {
|
||||||
PlusCircle, Users, Swords, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown,
|
PlusCircle, Users, Swords, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown,
|
||||||
UserCheck, UserX, HeartCrack, HeartPulse, Zap, EyeOff, ExternalLink, AlertTriangle,
|
UserCheck, UserX, HeartCrack, HeartPulse, Zap, EyeOff, ExternalLink, AlertTriangle,
|
||||||
@@ -201,32 +201,22 @@ function useFirestoreDocument(docPath) {
|
|||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!db || !docPath) {
|
if (!docPath) {
|
||||||
setData(null);
|
setData(null);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setError(docPath ? "Firestore not available." : "Document path not provided.");
|
setError("Document path not provided.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const docRef = doc(db, docPath);
|
const storage = getStorage();
|
||||||
const unsubscribe = onSnapshot(
|
const unsubscribe = storage.subscribeDoc(docPath, (doc) => {
|
||||||
docRef,
|
setData(doc);
|
||||||
(docSnap) => {
|
setIsLoading(false);
|
||||||
setData(docSnap.exists() ? { id: docSnap.id, ...docSnap.data() } : null);
|
});
|
||||||
setIsLoading(false);
|
return () => { if (typeof unsubscribe === 'function') unsubscribe(); };
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
console.error(`Error fetching document ${docPath}:`, err);
|
|
||||||
setError(err.message || "Failed to fetch document.");
|
|
||||||
setIsLoading(false);
|
|
||||||
setData(null);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => unsubscribe();
|
|
||||||
}, [docPath]);
|
}, [docPath]);
|
||||||
|
|
||||||
return { data, isLoading, error };
|
return { data, isLoading, error };
|
||||||
@@ -239,34 +229,23 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) {
|
|||||||
const queryString = useMemo(() => JSON.stringify(queryConstraints), [queryConstraints]);
|
const queryString = useMemo(() => JSON.stringify(queryConstraints), [queryConstraints]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!db || !collectionPath) {
|
if (!collectionPath) {
|
||||||
setData([]);
|
setData([]);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setError(collectionPath ? "Firestore not available." : "Collection path not provided.");
|
setError("Collection path not provided.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const q = query(collection(db, collectionPath), ...queryConstraints);
|
const storage = getStorage();
|
||||||
const unsubscribe = onSnapshot(
|
const unsubscribe = storage.subscribeCollection(collectionPath, (items) => {
|
||||||
q,
|
setData(items);
|
||||||
(snapshot) => {
|
setIsLoading(false);
|
||||||
const items = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
|
});
|
||||||
setData(items);
|
return () => { if (typeof unsubscribe === 'function') unsubscribe(); };
|
||||||
setIsLoading(false);
|
// queryString, not array ref
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
console.error(`Error fetching collection ${collectionPath}:`, err);
|
|
||||||
setError(err.message || "Failed to fetch collection.");
|
|
||||||
setIsLoading(false);
|
|
||||||
setData([]);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => unsubscribe();
|
|
||||||
// We use queryString instead of queryConstraints to avoid re-renders on array reference changes
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [collectionPath, queryString]);
|
}, [collectionPath, queryString]);
|
||||||
|
|
||||||
|
|||||||
+41
-7
@@ -1,13 +1,47 @@
|
|||||||
// src/storage/index.js — barrel re-export of Firebase SDK.
|
// src/storage/index.js — storage factory + SDK re-exports.
|
||||||
// Phase C: App.js swaps imports from 'firebase/*' to here.
|
// STORAGE=firebase (default): adapter wraps SDK. STORAGE=ws: backend.
|
||||||
// STORAGE=firebase = identical behavior. Zero risk.
|
// App.js imports getStorage() for subscribe; still imports SDK pieces for writes (per-group refactor pending).
|
||||||
// Later: factory picks impl based on REACT_APP_STORAGE env.
|
|
||||||
|
|
||||||
export { initializeApp } from 'firebase/app';
|
import { initializeApp } from 'firebase/app';
|
||||||
export {
|
import {
|
||||||
getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken,
|
getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken,
|
||||||
} from 'firebase/auth';
|
} from 'firebase/auth';
|
||||||
export {
|
import {
|
||||||
getFirestore, doc, setDoc, getDoc, getDocs, addDoc, collection,
|
getFirestore, doc, setDoc, getDoc, getDocs, addDoc, collection,
|
||||||
onSnapshot, updateDoc, deleteDoc, query, orderBy, limit, writeBatch,
|
onSnapshot, updateDoc, deleteDoc, query, orderBy, limit, writeBatch,
|
||||||
} from 'firebase/firestore';
|
} from 'firebase/firestore';
|
||||||
|
import { initFirebase, createFirebaseStorage } from './firebase';
|
||||||
|
|
||||||
|
let storageInstance = null;
|
||||||
|
|
||||||
|
// Returns adapter instance implementing interface (getDoc/setDoc/subscribeDoc/etc).
|
||||||
|
export function getStorage() {
|
||||||
|
if (storageInstance) return storageInstance;
|
||||||
|
const mode = process.env.REACT_APP_STORAGE || 'firebase';
|
||||||
|
if (mode === 'firebase') {
|
||||||
|
const ok = initFirebase();
|
||||||
|
if (!ok) throw new Error('Firebase config missing. Check REACT_APP_FIREBASE_* env.');
|
||||||
|
storageInstance = createFirebaseStorage();
|
||||||
|
} else if (mode === 'ws') {
|
||||||
|
const { createWsStorage } = require('./ws');
|
||||||
|
storageInstance = createWsStorage({
|
||||||
|
baseUrl: process.env.REACT_APP_BACKEND_URL || 'http://127.0.0.1:4001',
|
||||||
|
wsUrl: process.env.REACT_APP_BACKEND_WS || 'ws://127.0.0.1:4001/ws',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const { createMemoryStorage } = require('./memory');
|
||||||
|
storageInstance = createMemoryStorage();
|
||||||
|
}
|
||||||
|
return storageInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStorageMode() {
|
||||||
|
return process.env.REACT_APP_STORAGE || 'firebase';
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
initializeApp,
|
||||||
|
getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken,
|
||||||
|
getFirestore, doc, setDoc, getDoc, getDocs, addDoc, collection,
|
||||||
|
onSnapshot, updateDoc, deleteDoc, query, orderBy, limit, writeBatch,
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user