diff --git a/examples/react/kitchen-sink/README.md b/examples/react/kitchen-sink/README.md new file mode 100644 index 00000000..07d7fe34 --- /dev/null +++ b/examples/react/kitchen-sink/README.md @@ -0,0 +1,60 @@ +# TanStack Query Firebase Examples + +A comprehensive example application showcasing various TanStack Query Firebase hooks and patterns. + +## Features + +- **Authentication Examples**: ID token management with `useGetIdTokenQuery` +- **Firestore Examples**: Collection querying with `useCollectionQuery` +- **Real-time Updates**: See how the UI updates when data changes +- **Mutation Integration**: Add/delete operations with proper error handling +- **Loading States**: Proper loading and error state management +- **Query Key Management**: Dynamic query keys based on filters + +## Running the Examples + +1. Start the Firebase emulators: + ```bash + cd ../../../ && firebase emulators:start + ``` + +2. In another terminal, run the example app: + ```bash + pnpm dev:emulator + ``` + +3. Navigate to different examples using the navigation bar: + - **Home**: Overview of available examples + - **ID Token Query**: Firebase Authentication token management + - **Collection Query**: Firestore collection querying with filters + +## Key Concepts Demonstrated + +- Using `useGetIdTokenQuery` for Firebase Authentication +- Using `useCollectionQuery` with different query configurations +- Combining queries with mutations (`useAddDocumentMutation`, `useDeleteDocumentMutation`) +- Dynamic query keys for filtered results +- Proper TypeScript integration with Firestore data +- React Router for navigation between examples + +## File Structure + +``` +src/ +├── components/ +│ ├── IdTokenExample.tsx # Authentication example +│ └── CollectionQueryExample.tsx # Firestore example +├── App.tsx # Main app with routing +├── firebase.ts # Firebase initialization +├── main.tsx # Entry point +└── index.css # Tailwind CSS +``` + +## Technologies Used + +- **Vite**: Fast build tool and dev server +- **React Router**: Client-side routing +- **TanStack Query**: Data fetching and caching +- **Firebase**: Authentication and Firestore +- **Tailwind CSS**: Utility-first styling +- **TypeScript**: Type safety diff --git a/examples/react/useGetIdTokenQuery/index.html b/examples/react/kitchen-sink/index.html similarity index 86% rename from examples/react/useGetIdTokenQuery/index.html rename to examples/react/kitchen-sink/index.html index e4b78eae..55112303 100644 --- a/examples/react/useGetIdTokenQuery/index.html +++ b/examples/react/kitchen-sink/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + TanStack Query Firebase Examples
diff --git a/examples/react/useGetIdTokenQuery/package.json b/examples/react/kitchen-sink/package.json similarity index 81% rename from examples/react/useGetIdTokenQuery/package.json rename to examples/react/kitchen-sink/package.json index 39d53f05..855b5529 100644 --- a/examples/react/useGetIdTokenQuery/package.json +++ b/examples/react/kitchen-sink/package.json @@ -1,11 +1,11 @@ { - "name": "useGetIdTokenQuery", + "name": "firebase-examples", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", - "dev:emulator": "cd ../../../ && firebase emulators:exec --project test-project 'cd examples/react/useGetIdTokenQuery && vite'", + "dev:emulator": "cd ../../../ && firebase emulators:exec --project test-project 'cd examples/react/firebase-examples && vite'", "build": "npx vite build", "preview": "vite preview" }, @@ -15,7 +15,8 @@ "@tanstack/react-query-devtools": "^5.84.2", "firebase": "^11.3.1", "react": "^19.1.1", - "react-dom": "^19.1.1" + "react-dom": "^19.1.1", + "react-router-dom": "^6.28.0" }, "devDependencies": { "@types/react": "^19.1.9", diff --git a/examples/react/useGetIdTokenQuery/postcss.config.js b/examples/react/kitchen-sink/postcss.config.js similarity index 100% rename from examples/react/useGetIdTokenQuery/postcss.config.js rename to examples/react/kitchen-sink/postcss.config.js diff --git a/examples/react/kitchen-sink/src/App.tsx b/examples/react/kitchen-sink/src/App.tsx new file mode 100644 index 00000000..851882b9 --- /dev/null +++ b/examples/react/kitchen-sink/src/App.tsx @@ -0,0 +1,181 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { useState } from "react"; +import { Link, Route, Routes, useLocation } from "react-router-dom"; +import { CollectionQueryExample } from "./components/CollectionQueryExample"; +import { IdTokenExample } from "./components/IdTokenExample"; +import { NestedCollectionsExample } from "./components/NestedCollectionsExample"; +import { WithConverterExample } from "./components/WithConverterExample"; + +import "./firebase"; + +function App() { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, + }, + }, + }), + ); + + return ( + +
+ +
+
+ + } /> + } /> + } + /> + } + /> + } + /> + +
+
+
+ +
+ ); +} + +function Navigation() { + const location = useLocation(); + + const isActive = (path: string) => location.pathname === path; + + return ( + + ); +} + +function Home() { + return ( +
+

+ TanStack Query Firebase Examples +

+

+ Explore different patterns and use cases for Firebase with TanStack + Query +

+
+ +

+ Auth: ID Token +

+

+ Get and refresh Firebase ID tokens with proper caching +

+ + +

+ Firestore: Collection Query +

+

+ Query Firestore collections with filtering and mutations +

+ + +

+ Nested Collections +

+

+ Handle nested Firestore collections with real-time updates +

+ + +

+ TypeScript Safety +

+

+ Resolve DocumentData vs custom interface type issues +

+ +
+
+ ); +} + +export default App; diff --git a/examples/react/kitchen-sink/src/components/CollectionQueryExample.tsx b/examples/react/kitchen-sink/src/components/CollectionQueryExample.tsx new file mode 100644 index 00000000..358d766e --- /dev/null +++ b/examples/react/kitchen-sink/src/components/CollectionQueryExample.tsx @@ -0,0 +1,329 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { + useAddDocumentMutation, + useCollectionQuery, +} from "@tanstack-query-firebase/react/firestore"; +import { + collection, + deleteDoc, + doc, + getFirestore, + query, + Timestamp, + where, +} from "firebase/firestore"; +import { useState } from "react"; + +interface Task { + id: string; + title: string; + completed: boolean; + priority: "low" | "medium" | "high"; + createdAt: Timestamp | Date; +} + +export function CollectionQueryExample() { + const [newTaskTitle, setNewTaskTitle] = useState(""); + const [newTaskPriority, setNewTaskPriority] = + useState("medium"); + const [filterCompleted, setFilterCompleted] = useState(null); + + const queryClient = useQueryClient(); + const firestore = getFirestore(); + const tasksCollection = collection(firestore, "tasks"); + + // Create query based on filter + const tasksQuery = + filterCompleted !== null + ? query(tasksCollection, where("completed", "==", filterCompleted)) + : tasksCollection; + + // Query all tasks + const { + data: tasksSnapshot, + isLoading, + isError, + error, + } = useCollectionQuery(tasksQuery, { + queryKey: ["tasks", filterCompleted], + }); + + // Add task mutation with query invalidation + const addTaskMutation = useAddDocumentMutation(tasksCollection, { + onSuccess: () => { + // Invalidate all task queries after successful add + queryClient.invalidateQueries({ queryKey: ["tasks"] }); + }, + }); + + const handleAddTask = async () => { + if (!newTaskTitle.trim()) return; + + const newTask = { + title: newTaskTitle.trim(), + completed: false, + priority: newTaskPriority, + createdAt: new Date(), + }; + + try { + await addTaskMutation.mutateAsync(newTask); + setNewTaskTitle(""); + setNewTaskPriority("medium"); + } catch (error) { + console.error("Failed to add task:", error); + } + }; + + const handleToggleTask = async ( + taskId: string, + currentCompleted: boolean, + ) => { + // Note: In a real app, you'd use useUpdateDocumentMutation here + // For simplicity, we're just showing the query functionality + console.log(`Would toggle task ${taskId} to ${!currentCompleted}`); + }; + + // Use a mutation for delete with dynamic document reference TODO: why doesn't the library support dynamic document reference? + const deleteTaskMutation = useMutation({ + mutationFn: async (taskId: string) => { + const taskRef = doc(firestore, "tasks", taskId); + await deleteDoc(taskRef); + }, + onSuccess: () => { + // Invalidate all task queries after successful delete + queryClient.invalidateQueries({ queryKey: ["tasks"] }); + }, + }); + + const handleDeleteTask = async (taskId: string) => { + try { + await deleteTaskMutation.mutateAsync(taskId); + } catch (error) { + console.error("Failed to delete task:", error); + } + }; + + const tasks = + (tasksSnapshot?.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as Task[]) || []; + + const getPriorityColor = (priority: Task["priority"]) => { + switch (priority) { + case "high": + return "text-red-600 bg-red-100"; + case "medium": + return "text-yellow-600 bg-yellow-100"; + case "low": + return "text-green-600 bg-green-100"; + } + }; + + return ( +
+

+ Task Management with useCollectionQuery +

+ + {/* Add Task Form */} +
+

Add New Task

+
+
+ + setNewTaskTitle(e.target.value)} + placeholder="Enter task title..." + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + onKeyPress={(e) => e.key === "Enter" && handleAddTask()} + /> +
+
+ + +
+ +
+
+ + {/* Filter Controls */} +
+ Filter: + + + +
+ + {/* Query Status */} +
+ {isLoading && ( +
+
+

Loading tasks...

+
+ )} + + {isError && ( +
+

Error loading tasks

+

+ {error?.message || "An unknown error occurred"} +

+
+ )} +
+ + {/* Tasks List */} + {!isLoading && !isError && ( +
+ {tasks.length === 0 ? ( +
+ {filterCompleted === null + ? "No tasks found. Add your first task above!" + : `No ${ + filterCompleted ? "completed" : "pending" + } tasks found.`} +
+ ) : ( + tasks.map((task) => ( +
+
+ handleToggleTask(task.id, task.completed)} + className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + /> +
+

+ {task.title} +

+

+ Created:{" "} + {task.createdAt instanceof Timestamp + ? task.createdAt.toDate().toLocaleDateString() + : task.createdAt instanceof Date + ? task.createdAt.toLocaleDateString() + : "Unknown date"} +

+
+ + {task.priority} + +
+ +
+ )) + )} +
+ )} + + {/* Query Info */} +
+

Query Information

+
+

+ Query Key:{" "} + {JSON.stringify(["tasks", filterCompleted])} +

+

+ Total Tasks: {tasks.length} +

+

+ Filter:{" "} + {filterCompleted === null + ? "All" + : filterCompleted + ? "Completed" + : "Pending"} +

+

+ Status:{" "} + {isLoading ? "Loading" : isError ? "Error" : "Success"} +

+
+
+
+ ); +} diff --git a/examples/react/useGetIdTokenQuery/src/components/IdTokenExample.tsx b/examples/react/kitchen-sink/src/components/IdTokenExample.tsx similarity index 98% rename from examples/react/useGetIdTokenQuery/src/components/IdTokenExample.tsx rename to examples/react/kitchen-sink/src/components/IdTokenExample.tsx index 6c60ccd1..b05beb37 100644 --- a/examples/react/useGetIdTokenQuery/src/components/IdTokenExample.tsx +++ b/examples/react/kitchen-sink/src/components/IdTokenExample.tsx @@ -129,7 +129,7 @@ export function IdTokenExample() { ) : (
-

+

Token hash: {token ? `${btoa(token).slice(0, 16)}...` : ""}

{lastRefreshTime && ( @@ -170,7 +170,7 @@ export function IdTokenExample() {

Loading fresh token...

) : freshToken ? (
-

+

Token hash:{" "} {freshToken ? `${btoa(freshToken).slice(0, 16)}...` : ""}

diff --git a/examples/react/kitchen-sink/src/components/NestedCollectionsExample.tsx b/examples/react/kitchen-sink/src/components/NestedCollectionsExample.tsx new file mode 100644 index 00000000..79fd2299 --- /dev/null +++ b/examples/react/kitchen-sink/src/components/NestedCollectionsExample.tsx @@ -0,0 +1,606 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { + useAddDocumentMutation, + useCollectionQuery, +} from "@tanstack-query-firebase/react/firestore"; +import { + addDoc, + collection, + deleteDoc, + doc, + getFirestore, + limit, + orderBy, + query, + updateDoc, + where, +} from "firebase/firestore"; +import { useState } from "react"; + +interface ChatMessage { + id: string; + text: string; + senderId: string; + senderName: string; + timestamp: Date; +} + +interface Conversation { + id: string; + topic: string; + description: string; + members: string[]; + isConcluded: boolean; + createdAt: Date; + lastMessageAt: Date; + chatMessages?: ChatMessage[]; +} + +export function NestedCollectionsExample() { + const [newConversationTopic, setNewConversationTopic] = useState(""); + const [newConversationDescription, setNewConversationDescription] = + useState(""); + const [selectedConversationId, setSelectedConversationId] = useState< + string | null + >(null); + const [newMessageText, setNewMessageText] = useState(""); + const [filterConcluded, setFilterConcluded] = useState(null); + + const queryClient = useQueryClient(); + const firestore = getFirestore(); + const conversationsCollection = collection(firestore, "conversations"); + + // Query conversations with real-time updates + const conversationsQuery = + filterConcluded !== null + ? query( + conversationsCollection, + where("isConcluded", "==", filterConcluded), + orderBy("lastMessageAt", "desc"), + ) + : query(conversationsCollection, orderBy("lastMessageAt", "desc")); + + const { + data: conversationsSnapshot, + isLoading: conversationsLoading, + isError: conversationsError, + error: conversationsErrorData, + } = useCollectionQuery(conversationsQuery, { + queryKey: ["conversations", filterConcluded], + subscribed: true, // Enable real-time updates + }); + + // Query chat messages for selected conversation with real-time updates + const chatMessagesQuery = selectedConversationId + ? query( + collection( + firestore, + "conversations", + selectedConversationId, + "chatMessages", + ), + orderBy("timestamp", "asc"), + limit(50), + ) + : null; + + const { + data: messagesSnapshot, + isLoading: messagesLoading, + isError: messagesError, + error: messagesErrorData, + } = useCollectionQuery(chatMessagesQuery!, { + queryKey: ["chatMessages", selectedConversationId], + enabled: !!selectedConversationId && !!chatMessagesQuery, + subscribed: true, // Enable real-time updates + }); + + // Mutations + const addConversationMutation = useAddDocumentMutation( + conversationsCollection, + { + onSuccess: () => { + // Invalidate conversations query to refresh the list + queryClient.invalidateQueries({ queryKey: ["conversations"] }); + }, + onError: (error) => { + console.error("Failed to add conversation:", error); + // Could show a toast notification here + }, + }, + ); + + // Custom mutation for adding messages with proper invalidation + const addMessageMutation = useMutation({ + mutationFn: async (newMessage: Omit) => { + if (!selectedConversationId) { + throw new Error("No conversation selected"); + } + const messagesCollection = collection( + firestore, + "conversations", + selectedConversationId, + "chatMessages", + ); + return addDoc(messagesCollection, newMessage); + }, + onMutate: async (newMessage) => { + // Cancel in-flight queries + await queryClient.cancelQueries({ + queryKey: ["chatMessages", selectedConversationId], + }); + + // Store the actual snapshot structure + const previousSnapshot = queryClient.getQueryData([ + "chatMessages", + selectedConversationId, + ]); + + // Create a temporary message with proper structure + const tempMessage = { + id: `temp-${Date.now()}`, + ...newMessage, + timestamp: new Date(), + }; + + // Update maintaining the snapshot structure + queryClient.setQueryData( + ["chatMessages", selectedConversationId], + (old: any) => { + if (!old) return old; + + // Create a new doc-like object + const newDoc = { + id: tempMessage.id, + data: () => tempMessage, + // Include other doc methods if needed + }; + + return { + ...old, + docs: [...(old.docs || []), newDoc], + }; + }, + ); + + return { previousSnapshot }; + }, + onError: (error, _variables, context) => { + // Show user-friendly error message + console.error("Failed to send message:", error); + // Could show a toast notification here + + // Rollback optimistic update + if (context?.previousSnapshot) { + queryClient.setQueryData( + ["chatMessages", selectedConversationId], + context.previousSnapshot, + ); + } + }, + onSuccess: async () => { + // Update conversation's lastMessageAt + if (selectedConversationId) { + try { + await updateDoc( + doc(firestore, "conversations", selectedConversationId), + { + lastMessageAt: new Date(), + }, + ); + } catch (error) { + console.error("Failed to update conversation timestamp:", error); + } + } + + // Invalidate both queries + queryClient.invalidateQueries({ queryKey: ["conversations"] }); + queryClient.invalidateQueries({ + queryKey: ["chatMessages", selectedConversationId], + }); + }, + }); + + // Custom mutation for deleting conversations + const deleteConversationMutation = useMutation({ + mutationFn: async (conversationId: string) => { + const conversationRef = doc(firestore, "conversations", conversationId); + return deleteDoc(conversationRef); + }, + onError: (error, _conversationId) => { + console.error("Failed to delete conversation:", error); + // Could show a toast notification here + }, + onSuccess: (_, conversationId) => { + // Invalidate conversations query + queryClient.invalidateQueries({ queryKey: ["conversations"] }); + + // Clear messages if this was the selected conversation + if (selectedConversationId === conversationId) { + queryClient.removeQueries({ + queryKey: ["chatMessages", conversationId], + }); + setSelectedConversationId(null); + } + }, + }); + + const handleAddConversation = async () => { + if (!newConversationTopic.trim()) return; + + const newConversation = { + topic: newConversationTopic.trim(), + description: newConversationDescription.trim(), + members: ["user1", "user2"], // In real app, this would be actual user IDs + isConcluded: false, + createdAt: new Date(), + lastMessageAt: new Date(), + }; + + try { + await addConversationMutation.mutateAsync(newConversation); + setNewConversationTopic(""); + setNewConversationDescription(""); + } catch (error) { + console.error("Failed to add conversation:", error); + } + }; + + const handleAddMessage = async () => { + if (!selectedConversationId || !newMessageText.trim()) return; + + const newMessage = { + text: newMessageText.trim(), + senderId: "user1", // In real app, this would be the current user's ID + senderName: "Current User", + timestamp: new Date(), + }; + + try { + await addMessageMutation.mutateAsync(newMessage); + setNewMessageText(""); + } catch (error) { + console.error("Failed to add message:", error); + } + }; + + const handleDeleteConversation = async (conversationId: string) => { + try { + await deleteConversationMutation.mutateAsync(conversationId); + } catch (error) { + console.error("Failed to delete conversation:", error); + } + }; + + // Proper date serialization + const conversations = + (conversationsSnapshot?.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + // Convert Firestore Timestamps to Dates + createdAt: + doc.data().createdAt?.toDate?.() || doc.data().createdAt || new Date(), + lastMessageAt: + doc.data().lastMessageAt?.toDate?.() || + doc.data().lastMessageAt || + new Date(), + })) as Conversation[]) || []; + + const messages = + (messagesSnapshot?.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + // Convert Firestore Timestamps to Dates + timestamp: + doc.data().timestamp?.toDate?.() || doc.data().timestamp || new Date(), + })) as ChatMessage[]) || []; + + const selectedConversation = conversations.find( + (conv) => conv.id === selectedConversationId, + ); + + return ( +
+

+ Nested Collections: Conversations & Chat Messages +

+ + {/* Add Conversation Form */} +
+

Add New Conversation

+
+
+ + setNewConversationTopic(e.target.value)} + placeholder="Enter conversation topic..." + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + onKeyPress={(e) => e.key === "Enter" && handleAddConversation()} + /> +
+
+ + setNewConversationDescription(e.target.value)} + placeholder="Enter description..." + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ +
+
+ + {/* Filter Controls */} +
+ Filter: + + + +
+ +
+ {/* Conversations List */} +
+

Conversations

+ + {conversationsLoading && ( +
+
+

Loading conversations...

+
+ )} + + {conversationsError && ( +
+

+ Error loading conversations +

+

+ {conversationsErrorData?.message || "An unknown error occurred"} +

+
+ )} + + {!conversationsLoading && !conversationsError && ( +
+ {conversations.length === 0 ? ( +
+ No conversations found. Add your first conversation above! +
+ ) : ( + conversations.map((conversation) => ( + +
+
+ + )) + )} +
+ )} +
+ + {/* Chat Messages */} +
+

+ {selectedConversation + ? `Chat: ${selectedConversation.topic}` + : "Select a conversation"} +

+ + {selectedConversationId ? ( + <> + {messagesLoading && ( +
+
+

Loading messages...

+
+ )} + + {messagesError && ( +
+

+ Error loading messages +

+

+ {messagesErrorData?.message || "An unknown error occurred"} +

+
+ )} + + {!messagesLoading && !messagesError && ( + <> + {/* Messages List */} +
+ {messages.length === 0 ? ( +
+ No messages yet. Start the conversation! +
+ ) : ( + messages.map((message) => ( +
+
+ + {message.senderName} + + + {message.timestamp.toLocaleTimeString()} + +
+

{message.text}

+
+ )) + )} +
+ + {/* Add Message Form */} +
+ setNewMessageText(e.target.value)} + placeholder="Type a message..." + className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + onKeyPress={(e) => + e.key === "Enter" && handleAddMessage() + } + /> + +
+ + )} + + ) : ( +
+ Select a conversation to view messages +
+ )} +
+
+ + {/* Query Info */} +
+

Query Information

+
+

+ Conversations Query Key:{" "} + {JSON.stringify(["conversations", filterConcluded])} +

+

+ Messages Query Key:{" "} + {selectedConversationId + ? JSON.stringify(["chatMessages", selectedConversationId]) + : "Not selected"} +

+

+ Total Conversations: {conversations.length} +

+

+ Total Messages: {messages.length} +

+

+ Filter:{" "} + {filterConcluded === null + ? "All" + : filterConcluded + ? "Concluded" + : "Active"} +

+

+ Real-time Updates: Enabled for both queries +

+

+ Optimistic Updates: Enabled for message additions +

+

+ Query Invalidation: Automatic after mutations +

+

+ Error Handling: Rollback on mutation failures +

+
+
+
+ ); +} diff --git a/examples/react/kitchen-sink/src/components/WithConverterExample.tsx b/examples/react/kitchen-sink/src/components/WithConverterExample.tsx new file mode 100644 index 00000000..454c95f7 --- /dev/null +++ b/examples/react/kitchen-sink/src/components/WithConverterExample.tsx @@ -0,0 +1,67 @@ +import { useCollectionQuery } from "@tanstack-query-firebase/react/firestore"; +import { + collection, + type DocumentData, + getFirestore, + type QueryDocumentSnapshot, + query, + type SnapshotOptions, +} from "firebase/firestore"; + +type Product = { + name: string; + price: number; +}; + +const productConverter = { + toFirestore(product: Product): DocumentData { + return product; + }, + fromFirestore( + snapshot: QueryDocumentSnapshot, + options: SnapshotOptions, + ): Product { + const data = snapshot.data(options); + return { + name: data.name, + price: data.price, + }; + }, +}; + +export function WithConverterExample() { + const firestore = getFirestore(); + const ref = query( + collection(firestore, "products").withConverter(productConverter), + ); + + const { data, isLoading, isError, error } = useCollectionQuery< + Product, + DocumentData + >(ref, { + queryKey: ["products"], + }); + + if (isLoading) { + return
Loading...
; + } + + if (isError) { + return
Error: {error?.message}
; + } + + const products = data?.docs.map((doc) => doc.data()) ?? []; + + return ( +
+

Products

+
    + {products.map((product) => ( +
  • + {product.name} - ${product.price} +
  • + ))} +
+
+ ); +} diff --git a/examples/react/kitchen-sink/src/firebase.ts b/examples/react/kitchen-sink/src/firebase.ts new file mode 100644 index 00000000..baa30227 --- /dev/null +++ b/examples/react/kitchen-sink/src/firebase.ts @@ -0,0 +1,27 @@ +import { getApps, initializeApp } from "firebase/app"; +import { connectAuthEmulator, getAuth } from "firebase/auth"; +import { connectFirestoreEmulator, getFirestore } from "firebase/firestore"; + +if (getApps().length === 0) { + initializeApp({ + projectId: "test-project", + apiKey: "demo-api-key", // Required for Firebase to initialize + }); + + // Connect to emulators if running locally + if (import.meta.env.DEV) { + try { + // Connect to Auth emulator + const auth = getAuth(); + connectAuthEmulator(auth, "http://localhost:9099"); + console.log("Connected to Firebase Auth emulator"); + + // Connect to Firestore emulator + const firestore = getFirestore(); + connectFirestoreEmulator(firestore, "localhost", 8080); + console.log("Connected to Firebase Firestore emulator"); + } catch (error) { + console.warn("Could not connect to Firebase emulators:", error); + } + } +} diff --git a/examples/react/useGetIdTokenQuery/src/index.css b/examples/react/kitchen-sink/src/index.css similarity index 100% rename from examples/react/useGetIdTokenQuery/src/index.css rename to examples/react/kitchen-sink/src/index.css diff --git a/examples/react/useGetIdTokenQuery/src/main.tsx b/examples/react/kitchen-sink/src/main.tsx similarity index 67% rename from examples/react/useGetIdTokenQuery/src/main.tsx rename to examples/react/kitchen-sink/src/main.tsx index eff7ccc6..b17a076d 100644 --- a/examples/react/useGetIdTokenQuery/src/main.tsx +++ b/examples/react/kitchen-sink/src/main.tsx @@ -1,10 +1,13 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; import "./index.css"; import App from "./App.tsx"; createRoot(document.getElementById("root")!).render( - + + + , ); diff --git a/examples/react/useGetIdTokenQuery/tailwind.config.js b/examples/react/kitchen-sink/tailwind.config.js similarity index 100% rename from examples/react/useGetIdTokenQuery/tailwind.config.js rename to examples/react/kitchen-sink/tailwind.config.js diff --git a/examples/react/useGetIdTokenQuery/tsconfig.json b/examples/react/kitchen-sink/tsconfig.json similarity index 64% rename from examples/react/useGetIdTokenQuery/tsconfig.json rename to examples/react/kitchen-sink/tsconfig.json index 31cb5a0c..df2ec7d1 100644 --- a/examples/react/useGetIdTokenQuery/tsconfig.json +++ b/examples/react/kitchen-sink/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "target": "ES2022", + "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2022", "DOM", "DOM.Iterable"], + "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, @@ -18,7 +18,11 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + + /* Types */ + "types": ["vite/client"] }, - "include": ["src", "vite.config.ts"] + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/examples/react/kitchen-sink/tsconfig.node.json b/examples/react/kitchen-sink/tsconfig.node.json new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/examples/react/kitchen-sink/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/react/useGetIdTokenQuery/vite.config.ts b/examples/react/kitchen-sink/vite.config.ts similarity index 53% rename from examples/react/useGetIdTokenQuery/vite.config.ts rename to examples/react/kitchen-sink/vite.config.ts index 8c136be8..6a1235bb 100644 --- a/examples/react/useGetIdTokenQuery/vite.config.ts +++ b/examples/react/kitchen-sink/vite.config.ts @@ -6,7 +6,11 @@ export default defineConfig({ plugins: [react()], build: { rollupOptions: { - external: ["@tanstack-query-firebase/react/auth"], + external: [ + "@tanstack-query-firebase/react/auth", + "@tanstack-query-firebase/react/firestore", + "@tanstack-query-firebase/react/data-connect", + ], }, }, }); diff --git a/examples/react/useGetIdTokenQuery/.gitignore b/examples/react/useGetIdTokenQuery/.gitignore deleted file mode 100644 index a547bf36..00000000 --- a/examples/react/useGetIdTokenQuery/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/examples/react/useGetIdTokenQuery/README.md b/examples/react/useGetIdTokenQuery/README.md deleted file mode 100644 index a2d0969c..00000000 --- a/examples/react/useGetIdTokenQuery/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Firebase Authentication Example (Vite) - -Simple Vite React app demonstrating Firebase Authentication with TanStack Query. - -## Quick Start - -```bash -# Install dependencies -pnpm install - -# Run with emulators (recommended) -pnpm dev:emulator - -# Or run without emulators -pnpm dev -``` - -## Features - -- **ID Token Management** - `useGetIdTokenQuery` hook demo - diff --git a/examples/react/useGetIdTokenQuery/postcss.config.mjs b/examples/react/useGetIdTokenQuery/postcss.config.mjs deleted file mode 100644 index 2ef30fcf..00000000 --- a/examples/react/useGetIdTokenQuery/postcss.config.mjs +++ /dev/null @@ -1,9 +0,0 @@ -/** @type {import('postcss-load-config').Config} */ -const config = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; - -export default config; diff --git a/examples/react/useGetIdTokenQuery/public/vite.svg b/examples/react/useGetIdTokenQuery/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/examples/react/useGetIdTokenQuery/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/react/useGetIdTokenQuery/src/App.tsx b/examples/react/useGetIdTokenQuery/src/App.tsx deleted file mode 100644 index 8374bbd8..00000000 --- a/examples/react/useGetIdTokenQuery/src/App.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; -import { useState } from "react"; -import { IdTokenExample } from "./components/IdTokenExample"; - -import "./firebase"; - -function App() { - const [queryClient] = useState( - () => - new QueryClient({ - defaultOptions: { - queries: { - staleTime: 60 * 1000, - }, - }, - }), - ); - - return ( - -
-
-
-

- Firebase Authentication Examples -

-

- TanStack Query Firebase Authentication hooks and patterns -

-
- -
- -
- -
-

- Built with Vite, TanStack Query, and Firebase Auth -

-
-
-
- -
- ); -} - -export default App; diff --git a/examples/react/useGetIdTokenQuery/src/firebase.ts b/examples/react/useGetIdTokenQuery/src/firebase.ts deleted file mode 100644 index 25843ade..00000000 --- a/examples/react/useGetIdTokenQuery/src/firebase.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { getApps, initializeApp } from "firebase/app"; -import { connectAuthEmulator, getAuth } from "firebase/auth"; - -if (getApps().length === 0) { - initializeApp({ - projectId: "example", - apiKey: "demo-api-key", // Required for Firebase to initialize - }); - - // Connect to Auth emulator if running locally - if (import.meta.env.DEV) { - try { - const auth = getAuth(); - connectAuthEmulator(auth, "http://localhost:9099"); - console.log("Connected to Firebase Auth emulator"); - } catch (error) { - console.warn("Could not connect to Firebase Auth emulator:", error); - } - } -} diff --git a/examples/react/useGetIdTokenQuery/src/vite-env.d.ts b/examples/react/useGetIdTokenQuery/src/vite-env.d.ts deleted file mode 100644 index 11f02fe2..00000000 --- a/examples/react/useGetIdTokenQuery/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/examples/react/useGetIdTokenQuery/tailwind.config.ts b/examples/react/useGetIdTokenQuery/tailwind.config.ts deleted file mode 100644 index e9a0944e..00000000 --- a/examples/react/useGetIdTokenQuery/tailwind.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Config } from "tailwindcss"; - -const config: Config = { - content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", - ], - theme: { - extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", - }, - }, - }, - plugins: [], -}; -export default config; diff --git a/firestore.rules b/firestore.rules index f10db163..b18e9f05 100644 --- a/firestore.rules +++ b/firestore.rules @@ -7,6 +7,28 @@ service cloud.firestore { allow create: if true; allow get: if true; } + + match /products/{document=**} { + allow read: if true; + allow write: if true; + allow create: if true; + allow get: if true; + } + + match /conversations/{document=**} { + allow read: if true; + allow write: if true; + allow create: if true; + allow get: if true; + } + + match /tasks/{document=**} { + allow read: if true; + allow write: if true; + allow create: if true; + allow get: if true; + } + // match /noread/{document=**} { // allow read: if false; // } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea3da364..14edd8ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,6 +64,55 @@ importers: specifier: ^10.14.0 || ^11.3.0 version: 11.3.1 + examples/react/kitchen-sink: + dependencies: + '@tanstack-query-firebase/react': + specifier: workspace:* + version: link:../../../packages/react + '@tanstack/react-query': + specifier: ^5.66.9 + version: 5.66.9(react@19.1.1) + '@tanstack/react-query-devtools': + specifier: ^5.84.2 + version: 5.84.2(@tanstack/react-query@5.66.9(react@19.1.1))(react@19.1.1) + firebase: + specifier: ^11.3.1 + version: 11.3.1 + react: + specifier: ^19.1.1 + version: 19.1.1 + react-dom: + specifier: ^19.1.1 + version: 19.1.1(react@19.1.1) + react-router-dom: + specifier: ^6.28.0 + version: 6.30.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + devDependencies: + '@types/react': + specifier: ^19.1.9 + version: 19.1.9 + '@types/react-dom': + specifier: ^19.1.7 + version: 19.1.7(@types/react@19.1.9) + '@vitejs/plugin-react': + specifier: ^4.7.0 + version: 4.7.0(vite@7.1.1(@types/node@20.17.19)(jiti@1.21.7)(yaml@2.7.0)) + autoprefixer: + specifier: ^10.4.21 + version: 10.4.21(postcss@8.5.6) + postcss: + specifier: ^8.5.6 + version: 8.5.6 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.17 + typescript: + specifier: ~5.8.3 + version: 5.8.3 + vite: + specifier: ^7.1.1 + version: 7.1.1(@types/node@20.17.19)(jiti@1.21.7)(yaml@2.7.0) + examples/react/react-data-connect: dependencies: '@dataconnect/default-connector': @@ -116,52 +165,6 @@ importers: specifier: ^5 version: 5.8.3 - examples/react/useGetIdTokenQuery: - dependencies: - '@tanstack-query-firebase/react': - specifier: workspace:* - version: link:../../../packages/react - '@tanstack/react-query': - specifier: ^5.66.9 - version: 5.66.9(react@19.1.1) - '@tanstack/react-query-devtools': - specifier: ^5.84.2 - version: 5.84.2(@tanstack/react-query@5.66.9(react@19.1.1))(react@19.1.1) - firebase: - specifier: ^11.3.1 - version: 11.3.1 - react: - specifier: ^19.1.1 - version: 19.1.1 - react-dom: - specifier: ^19.1.1 - version: 19.1.1(react@19.1.1) - devDependencies: - '@types/react': - specifier: ^19.1.9 - version: 19.1.9 - '@types/react-dom': - specifier: ^19.1.7 - version: 19.1.7(@types/react@19.1.9) - '@vitejs/plugin-react': - specifier: ^4.7.0 - version: 4.7.0(vite@7.1.1(@types/node@20.17.19)(jiti@1.21.7)(yaml@2.7.0)) - autoprefixer: - specifier: ^10.4.21 - version: 10.4.21(postcss@8.5.6) - postcss: - specifier: ^8.5.6 - version: 8.5.6 - tailwindcss: - specifier: ^3.4.17 - version: 3.4.17 - typescript: - specifier: ~5.8.3 - version: 5.8.3 - vite: - specifier: ^7.1.1 - version: 7.1.1(@types/node@20.17.19)(jiti@1.21.7)(yaml@2.7.0) - packages/angular: dependencies: '@angular/common': @@ -1403,6 +1406,10 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@remix-run/router@1.23.0': + resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} + engines: {node: '>=14.0.0'} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -3437,6 +3444,19 @@ packages: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} + react-router-dom@6.30.1: + resolution: {integrity: sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.30.1: + resolution: {integrity: sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react@19.1.1: resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} engines: {node: '>=0.10.0'} @@ -5348,6 +5368,8 @@ snapshots: '@protobufjs/utf8@1.1.0': {} + '@remix-run/router@1.23.0': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/rollup-android-arm-eabi@4.34.8': @@ -5555,20 +5577,20 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.2 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.26.9 + '@babel/types': 7.28.2 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.2 '@types/babel__traverse@7.28.0': dependencies: @@ -7535,6 +7557,18 @@ snapshots: react-refresh@0.17.0: {} + react-router-dom@6.30.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + '@remix-run/router': 1.23.0 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-router: 6.30.1(react@19.1.1) + + react-router@6.30.1(react@19.1.1): + dependencies: + '@remix-run/router': 1.23.0 + react: 19.1.1 + react@19.1.1: {} read-cache@1.0.0: