From 5e3778cd442edf24626fa29786b8daf48b0e6301 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Mon, 18 Aug 2025 10:58:23 +0100 Subject: [PATCH 1/5] refactor(examples): made kitchen-sink example --- examples/react/kitchen-sink/README.md | 60 ++++ .../index.html | 2 +- .../package.json | 7 +- .../postcss.config.js | 0 examples/react/kitchen-sink/src/App.tsx | 133 ++++++++ .../src/components/CollectionQueryExample.tsx | 312 ++++++++++++++++++ .../src/components/IdTokenExample.tsx | 10 +- .../src/firebase.ts | 11 +- .../src/index.css | 0 .../src/main.tsx | 7 +- .../tailwind.config.js | 0 .../tsconfig.json | 7 +- .../react/kitchen-sink/tsconfig.node.json | 10 + .../vite.config.ts | 6 +- examples/react/useGetIdTokenQuery/.gitignore | 24 -- examples/react/useGetIdTokenQuery/README.md | 21 -- .../useGetIdTokenQuery/postcss.config.mjs | 9 - .../react/useGetIdTokenQuery/public/vite.svg | 1 - examples/react/useGetIdTokenQuery/src/App.tsx | 49 --- .../useGetIdTokenQuery/src/vite-env.d.ts | 1 - .../useGetIdTokenQuery/tailwind.config.ts | 20 -- pnpm-lock.yaml | 92 +++++- 22 files changed, 634 insertions(+), 148 deletions(-) create mode 100644 examples/react/kitchen-sink/README.md rename examples/react/{useGetIdTokenQuery => kitchen-sink}/index.html (86%) rename examples/react/{useGetIdTokenQuery => kitchen-sink}/package.json (81%) rename examples/react/{useGetIdTokenQuery => kitchen-sink}/postcss.config.js (100%) create mode 100644 examples/react/kitchen-sink/src/App.tsx create mode 100644 examples/react/kitchen-sink/src/components/CollectionQueryExample.tsx rename examples/react/{useGetIdTokenQuery => kitchen-sink}/src/components/IdTokenExample.tsx (98%) rename examples/react/{useGetIdTokenQuery => kitchen-sink}/src/firebase.ts (52%) rename examples/react/{useGetIdTokenQuery => kitchen-sink}/src/index.css (100%) rename examples/react/{useGetIdTokenQuery => kitchen-sink}/src/main.tsx (62%) rename examples/react/{useGetIdTokenQuery => kitchen-sink}/tailwind.config.js (100%) rename examples/react/{useGetIdTokenQuery => kitchen-sink}/tsconfig.json (76%) create mode 100644 examples/react/kitchen-sink/tsconfig.node.json rename examples/react/{useGetIdTokenQuery => kitchen-sink}/vite.config.ts (53%) delete mode 100644 examples/react/useGetIdTokenQuery/.gitignore delete mode 100644 examples/react/useGetIdTokenQuery/README.md delete mode 100644 examples/react/useGetIdTokenQuery/postcss.config.mjs delete mode 100644 examples/react/useGetIdTokenQuery/public/vite.svg delete mode 100644 examples/react/useGetIdTokenQuery/src/App.tsx delete mode 100644 examples/react/useGetIdTokenQuery/src/vite-env.d.ts delete mode 100644 examples/react/useGetIdTokenQuery/tailwind.config.ts 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..e1ae9e76 --- /dev/null +++ b/examples/react/kitchen-sink/src/App.tsx @@ -0,0 +1,133 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { useState } from "react"; +import { Routes, Route, Link, useLocation } from "react-router-dom"; +import { IdTokenExample } from "./components/IdTokenExample"; +import { CollectionQueryExample } from "./components/CollectionQueryExample"; + +import "./firebase"; + +function App() { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, + }, + }, + }) + ); + + return ( + +
+ +
+
+ + } /> + } /> + } + /> + +
+
+
+ +
+ ); +} + +function Navigation() { + const location = useLocation(); + + const navItems = [ + { path: "/", label: "Home" }, + { path: "/auth/id-token", label: "ID Token Query" }, + { path: "/firestore/collection-query", label: "Collection Query" }, + ]; + + return ( + + ); +} + +function Home() { + return ( +
+

+ TanStack Query Firebase Examples +

+

+ Explore different Firebase hooks and patterns with TanStack Query +

+ +
+
+

+ Authentication +

+

+ Examples of Firebase Authentication hooks including ID token + management. +

+ + View ID Token Example + +
+ +
+

Firestore

+

+ Examples of Firestore hooks for querying collections and documents. +

+ + View Collection Query Example + +
+
+ +
+

+ Built with Vite, TanStack Query, React Router, and Firebase +

+
+
+ ); +} + +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..290ce1e0 --- /dev/null +++ b/examples/react/kitchen-sink/src/components/CollectionQueryExample.tsx @@ -0,0 +1,312 @@ +import { + addDoc, + collection, + deleteDoc, + doc, + getFirestore, + query, + where, +} from "firebase/firestore"; +import { useState } from "react"; +import { + useCollectionQuery, + useAddDocumentMutation, + useDeleteDocumentMutation, +} from "@tanstack-query-firebase/react/firestore"; + +interface Task { + id: string; + title: string; + completed: boolean; + priority: "low" | "medium" | "high"; + createdAt: Date; +} + +export function CollectionQueryExample() { + const [newTaskTitle, setNewTaskTitle] = useState(""); + const [newTaskPriority, setNewTaskPriority] = + useState("medium"); + const [filterCompleted, setFilterCompleted] = useState(null); + + 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 + const addTaskMutation = useAddDocumentMutation(tasksCollection); + + // Delete task mutation + const deleteTaskMutation = useDeleteDocumentMutation(); + + 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 + ) => { + const taskRef = doc(firestore, "tasks", taskId); + // 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}`); + }; + + const handleDeleteTask = async (taskId: string) => { + const taskRef = doc(firestore, "tasks", taskId); + try { + await deleteTaskMutation.mutateAsync(taskRef); + } 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.toLocaleDateString()} +

+
+ + {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..c18d72cc 100644 --- a/examples/react/useGetIdTokenQuery/src/components/IdTokenExample.tsx +++ b/examples/react/kitchen-sink/src/components/IdTokenExample.tsx @@ -9,7 +9,7 @@ export function IdTokenExample() { const [refreshCount, setRefreshCount] = useState(0); const [previousToken, setPreviousToken] = useState(null); const [lastForceRefreshTime, setLastForceRefreshTime] = useState( - null, + null ); // Listen for auth state changes @@ -44,7 +44,7 @@ export function IdTokenExample() { if (token) { console.log( "Token retrieved successfully:", - `${token.substring(0, 20)}...`, + `${token.substring(0, 20)}...` ); // Check if token changed @@ -237,7 +237,7 @@ export function IdTokenExample() { const result = []; const maxLength = Math.max( token.length, - freshToken.length, + freshToken.length ); for (let i = 0; i < maxLength; i++) { @@ -252,13 +252,13 @@ export function IdTokenExample() { className="bg-yellow-300 text-red-600 font-bold" > {freshToken[i] || "∅"} - , + ); } else { result.push( {token[i]} - , + ); } } diff --git a/examples/react/useGetIdTokenQuery/src/firebase.ts b/examples/react/kitchen-sink/src/firebase.ts similarity index 52% rename from examples/react/useGetIdTokenQuery/src/firebase.ts rename to examples/react/kitchen-sink/src/firebase.ts index 25843ade..136e9b62 100644 --- a/examples/react/useGetIdTokenQuery/src/firebase.ts +++ b/examples/react/kitchen-sink/src/firebase.ts @@ -1,5 +1,6 @@ import { getApps, initializeApp } from "firebase/app"; import { connectAuthEmulator, getAuth } from "firebase/auth"; +import { connectFirestoreEmulator, getFirestore } from "firebase/firestore"; if (getApps().length === 0) { initializeApp({ @@ -7,14 +8,20 @@ if (getApps().length === 0) { apiKey: "demo-api-key", // Required for Firebase to initialize }); - // Connect to Auth emulator if running locally + // 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 Auth emulator:", 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 62% rename from examples/react/useGetIdTokenQuery/src/main.tsx rename to examples/react/kitchen-sink/src/main.tsx index eff7ccc6..85a16d58 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 76% rename from examples/react/useGetIdTokenQuery/tsconfig.json rename to examples/react/kitchen-sink/tsconfig.json index 31cb5a0c..a7fc6fbf 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, @@ -20,5 +20,6 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "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/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/pnpm-lock.yaml b/pnpm-lock.yaml index ea3da364..2e249990 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/firebase-examples: + 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,7 +165,7 @@ importers: specifier: ^5 version: 5.8.3 - examples/react/useGetIdTokenQuery: + examples/react/useCollectionQuery: dependencies: '@tanstack-query-firebase/react': specifier: workspace:* @@ -1403,6 +1452,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 +3490,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 +5414,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 +5623,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 +7603,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: From 64d2fedd57070de17a90306d4b196f18ae50af24 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Mon, 18 Aug 2025 12:47:55 +0100 Subject: [PATCH 2/5] refactor(examples): nested collection query improvements --- examples/react/kitchen-sink/src/App.tsx | 11 +- .../src/components/CollectionQueryExample.tsx | 25 +- .../src/components/IdTokenExample.tsx | 14 +- .../components/NestedCollectionsExample.tsx | 606 ++++++++++++++++++ examples/react/kitchen-sink/src/main.tsx | 2 +- 5 files changed, 631 insertions(+), 27 deletions(-) create mode 100644 examples/react/kitchen-sink/src/components/NestedCollectionsExample.tsx diff --git a/examples/react/kitchen-sink/src/App.tsx b/examples/react/kitchen-sink/src/App.tsx index e1ae9e76..b7a33935 100644 --- a/examples/react/kitchen-sink/src/App.tsx +++ b/examples/react/kitchen-sink/src/App.tsx @@ -1,9 +1,10 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { useState } from "react"; -import { Routes, Route, Link, useLocation } from "react-router-dom"; -import { IdTokenExample } from "./components/IdTokenExample"; +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 "./firebase"; @@ -16,7 +17,7 @@ function App() { staleTime: 60 * 1000, }, }, - }) + }), ); return ( @@ -32,6 +33,10 @@ function App() { path="/firestore/collection-query" element={} /> + } + /> diff --git a/examples/react/kitchen-sink/src/components/CollectionQueryExample.tsx b/examples/react/kitchen-sink/src/components/CollectionQueryExample.tsx index 290ce1e0..64bbcb73 100644 --- a/examples/react/kitchen-sink/src/components/CollectionQueryExample.tsx +++ b/examples/react/kitchen-sink/src/components/CollectionQueryExample.tsx @@ -1,5 +1,8 @@ import { - addDoc, + useAddDocumentMutation, + useCollectionQuery, +} from "@tanstack-query-firebase/react/firestore"; +import { collection, deleteDoc, doc, @@ -8,11 +11,6 @@ import { where, } from "firebase/firestore"; import { useState } from "react"; -import { - useCollectionQuery, - useAddDocumentMutation, - useDeleteDocumentMutation, -} from "@tanstack-query-firebase/react/firestore"; interface Task { id: string; @@ -50,9 +48,6 @@ export function CollectionQueryExample() { // Add task mutation const addTaskMutation = useAddDocumentMutation(tasksCollection); - // Delete task mutation - const deleteTaskMutation = useDeleteDocumentMutation(); - const handleAddTask = async () => { if (!newTaskTitle.trim()) return; @@ -74,9 +69,8 @@ export function CollectionQueryExample() { const handleToggleTask = async ( taskId: string, - currentCompleted: boolean + currentCompleted: boolean, ) => { - const taskRef = doc(firestore, "tasks", taskId); // 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}`); @@ -85,7 +79,7 @@ export function CollectionQueryExample() { const handleDeleteTask = async (taskId: string) => { const taskRef = doc(firestore, "tasks", taskId); try { - await deleteTaskMutation.mutateAsync(taskRef); + await deleteDoc(taskRef); } catch (error) { console.error("Failed to delete task:", error); } @@ -263,7 +257,7 @@ export function CollectionQueryExample() { {task.priority} @@ -271,7 +265,6 @@ export function CollectionQueryExample() { + + + + {/* 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/main.tsx b/examples/react/kitchen-sink/src/main.tsx index 85a16d58..b17a076d 100644 --- a/examples/react/kitchen-sink/src/main.tsx +++ b/examples/react/kitchen-sink/src/main.tsx @@ -9,5 +9,5 @@ createRoot(document.getElementById("root")!).render( - + , ); From d825178fd4b52625dd4ce211af45aee3820bd497 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Mon, 18 Aug 2025 12:50:59 +0100 Subject: [PATCH 3/5] chore: format --- .../components/NestedCollectionsExample.tsx | 4 +- pnpm-lock.yaml | 48 +------------------ 2 files changed, 3 insertions(+), 49 deletions(-) diff --git a/examples/react/kitchen-sink/src/components/NestedCollectionsExample.tsx b/examples/react/kitchen-sink/src/components/NestedCollectionsExample.tsx index 329f4813..79fd2299 100644 --- a/examples/react/kitchen-sink/src/components/NestedCollectionsExample.tsx +++ b/examples/react/kitchen-sink/src/components/NestedCollectionsExample.tsx @@ -165,7 +165,7 @@ export function NestedCollectionsExample() { return { previousSnapshot }; }, - onError: (error, variables, context) => { + onError: (error, _variables, context) => { // Show user-friendly error message console.error("Failed to send message:", error); // Could show a toast notification here @@ -207,7 +207,7 @@ export function NestedCollectionsExample() { const conversationRef = doc(firestore, "conversations", conversationId); return deleteDoc(conversationRef); }, - onError: (error, conversationId) => { + onError: (error, _conversationId) => { console.error("Failed to delete conversation:", error); // Could show a toast notification here }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e249990..14edd8ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,7 +64,7 @@ importers: specifier: ^10.14.0 || ^11.3.0 version: 11.3.1 - examples/react/firebase-examples: + examples/react/kitchen-sink: dependencies: '@tanstack-query-firebase/react': specifier: workspace:* @@ -165,52 +165,6 @@ importers: specifier: ^5 version: 5.8.3 - examples/react/useCollectionQuery: - 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': From cade209952781548c69b36483acb40f72676daf4 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Mon, 18 Aug 2025 13:26:47 +0100 Subject: [PATCH 4/5] docs(examples): add withConverter example --- examples/react/kitchen-sink/src/App.tsx | 159 +++++++++++------- .../src/components/WithConverterExample.tsx | 64 +++++++ firestore.rules | 22 +++ 3 files changed, 187 insertions(+), 58 deletions(-) create mode 100644 examples/react/kitchen-sink/src/components/WithConverterExample.tsx diff --git a/examples/react/kitchen-sink/src/App.tsx b/examples/react/kitchen-sink/src/App.tsx index b7a33935..851882b9 100644 --- a/examples/react/kitchen-sink/src/App.tsx +++ b/examples/react/kitchen-sink/src/App.tsx @@ -5,6 +5,7 @@ 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"; @@ -37,6 +38,10 @@ function App() { path="/nested-collections" element={} /> + } + /> @@ -49,35 +54,63 @@ function App() { function Navigation() { const location = useLocation(); - const navItems = [ - { path: "/", label: "Home" }, - { path: "/auth/id-token", label: "ID Token Query" }, - { path: "/firestore/collection-query", label: "Collection Query" }, - ]; + const isActive = (path: string) => location.pathname === path; return (