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 && (
+
+ )}
+
+ {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() {
Status:{" "}
diff --git a/examples/react/kitchen-sink/src/components/IdTokenExample.tsx b/examples/react/kitchen-sink/src/components/IdTokenExample.tsx
index c18d72cc..b05beb37 100644
--- a/examples/react/kitchen-sink/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
@@ -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)}...` : ""}
@@ -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/kitchen-sink/src/components/NestedCollectionsExample.tsx b/examples/react/kitchen-sink/src/components/NestedCollectionsExample.tsx
new file mode 100644
index 00000000..329f4813
--- /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()
+ }
+ />
+
+ {addMessageMutation.isPending ? "Sending..." : "Send"}
+
+
+ >
+ )}
+ >
+ ) : (
+
+ 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 (