├── .changeset ├── README.md └── config.json ├── .firebaserc ├── .github └── workflows │ ├── local-ci.yml │ ├── release.yml │ ├── test-react-firebase-v12.yml │ └── tests.yaml ├── .gitignore ├── .opensource └── project.json ├── .vscode ├── launch.json └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── biome.json ├── dataconnect-sdk └── js │ └── default-connector │ ├── .guides │ ├── config.json │ ├── setup.md │ └── usage.md │ ├── README.md │ ├── esm │ ├── index.esm.js │ └── package.json │ ├── index.cjs.js │ ├── index.d.ts │ └── package.json ├── dataconnect ├── connector │ ├── connector.yaml │ ├── mutations.gql │ └── queries.gql ├── dataconnect.yaml └── schema │ └── schema.gql ├── docs.json ├── docs ├── angular │ ├── data-connect │ │ ├── functions │ │ │ ├── injectDataConnectMutation.mdx │ │ │ └── injectDataConnectQuery.mdx │ │ ├── index.mdx │ │ ├── mutations.mdx │ │ └── querying.mdx │ └── index.mdx ├── index.mdx ├── react-query-firebase.mdx └── react │ ├── auth │ ├── hooks │ │ ├── useDeleteUserMutation.mdx │ │ ├── useGetIdTokenQuery.mdx │ │ ├── useReloadMutation.mdx │ │ ├── useSendSignInLinkToEmailMutation.mdx │ │ ├── useSignInAnonymouslyMutation.mdx │ │ ├── useSignInWithCredentialMutation.mdx │ │ ├── useSignInWithEmailAndPasswordMutation.mdx │ │ ├── useSignOutMutation.mdx │ │ ├── useUpdateCurrentUserMutation.mdx │ │ └── useVerifyPasswordResetCodeMutation.mdx │ └── index.mdx │ ├── data-connect │ ├── hooks │ │ ├── useDataConnectMutation.mdx │ │ └── useDataConnectQuery.mdx │ ├── index.mdx │ ├── mutations.mdx │ ├── querying.mdx │ └── server-side-rendering.mdx │ ├── firestore │ ├── hooks │ │ ├── useClearIndexedDbPersistenceMutation.mdx │ │ ├── useCollectionQuery.mdx │ │ ├── useDisableNetworkMutation.mdx │ │ ├── useDocumentQuery.mdx │ │ ├── useEnableNetworkMutation.mdx │ │ ├── useGetAggregateFromServerQuery.mdx │ │ ├── useGetCountFromServerQuery.mdx │ │ ├── useRunTransactionMutation.mdx │ │ └── useWaitForPendingWritesQuery.mdx │ └── index.mdx │ └── index.mdx ├── examples └── react │ ├── react-data-connect │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ │ ├── file.svg │ │ ├── globe.svg │ │ ├── next.svg │ │ ├── vercel.svg │ │ └── window.svg │ ├── src │ │ ├── app │ │ │ ├── layout.tsx │ │ │ ├── providers.tsx │ │ │ └── rsc │ │ │ │ └── data-connect │ │ │ │ └── page.tsx │ │ ├── examples │ │ │ └── data-connect.tsx │ │ ├── firebase.ts │ │ └── pages │ │ │ ├── _app.tsx │ │ │ ├── client │ │ │ └── data-connect.tsx │ │ │ └── ssr │ │ │ └── data-connect.tsx │ ├── tailwind.config.ts │ └── tsconfig.json │ └── useGetIdTokenQuery │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── postcss.config.mjs │ ├── public │ └── vite.svg │ ├── src │ ├── App.tsx │ ├── components │ │ └── IdTokenExample.tsx │ ├── firebase.ts │ ├── index.css │ ├── main.tsx │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── vite.config.ts ├── firebase.json ├── firestore.rules ├── functions ├── package.json └── src │ └── index.js ├── package.json ├── packages ├── angular │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── data-connect │ │ │ ├── index.ts │ │ │ ├── injectDataConnectMutation.test.ts │ │ │ ├── injectDataConnectQuery.test.ts │ │ │ └── types.ts │ │ └── index.ts │ ├── test-setup.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts └── react │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── analytics │ │ └── index.ts │ ├── auth │ │ ├── index.ts │ │ ├── types.ts │ │ ├── useApplyActionCodeMutation.test.tsx │ │ ├── useApplyActionCodeMutation.ts │ │ ├── useCheckActionCodeMutation.test.tsx │ │ ├── useCheckActionCodeMutation.ts │ │ ├── useConfirmPasswordResetMutation.test.tsx │ │ ├── useConfirmPasswordResetMutation.ts │ │ ├── useCreateUserWithEmailAndPasswordMutation.test.tsx │ │ ├── useCreateUserWithEmailAndPasswordMutation.ts │ │ ├── useDeleteUserMutation.test.tsx │ │ ├── useDeleteUserMutation.ts │ │ ├── useGetIdTokenQuery.test.tsx │ │ ├── useGetIdTokenQuery.ts │ │ ├── useGetRedirectResultQuery.test.tsx │ │ ├── useGetRedirectResultQuery.ts │ │ ├── useReloadMutation.test.tsx │ │ ├── useReloadMutation.ts │ │ ├── useRevokeAccessTokenMutation.test.tsx │ │ ├── useRevokeAccessTokenMutation.ts │ │ ├── useSendSignInLinkToEmailMutation.test.tsx │ │ ├── useSendSignInLinkToEmailMutation.ts │ │ ├── useSignInAnonymouslyMutation.test.tsx │ │ ├── useSignInAnonymouslyMutation.ts │ │ ├── useSignInWithCredentialMutation.test.tsx │ │ ├── useSignInWithCredentialMutation.ts │ │ ├── useSignInWithEmailAndPasswordMutation.test.tsx │ │ ├── useSignInWithEmailAndPasswordMutation.ts │ │ ├── useSignOutMutation.test.tsx │ │ ├── useSignOutMutation.ts │ │ ├── useUpdateCurrentUserMutation.test.tsx │ │ ├── useUpdateCurrentUserMutation.ts │ │ ├── useVerifyPasswordResetCodeMutation.test.tsx │ │ ├── useVerifyPasswordResetCodeMutation.ts │ │ └── utils.ts │ ├── data-connect │ │ ├── index.ts │ │ ├── query-client.ts │ │ ├── types.ts │ │ ├── useDataConnectMutation.test.tsx │ │ ├── useDataConnectMutation.ts │ │ ├── useDataConnectQuery.test.tsx │ │ ├── useDataConnectQuery.ts │ │ ├── utils.test.ts │ │ ├── utils.ts │ │ ├── validateReactArgs.test.tsx │ │ └── validateReactArgs.ts │ ├── database │ │ └── index.ts │ ├── firestore │ │ ├── index.ts │ │ ├── useAddDocumentMutation.test.tsx │ │ ├── useAddDocumentMutation.ts │ │ ├── useClearIndexedDbPersistenceMutation.test.tsx │ │ ├── useClearIndexedDbPersistenceMutation.ts │ │ ├── useCollectionQuery.test.tsx │ │ ├── useCollectionQuery.ts │ │ ├── useDeleteDocumentMutation.test.tsx │ │ ├── useDeleteDocumentMutation.ts │ │ ├── useDisableNetworkMutation.test.tsx │ │ ├── useDisableNetworkMutation.ts │ │ ├── useDocumentQuery.test.tsx │ │ ├── useDocumentQuery.ts │ │ ├── useEnableNetworkMutation.test.tsx │ │ ├── useEnableNetworkMutation.ts │ │ ├── useGetAggregateFromServerQuery.test.tsx │ │ ├── useGetAggregateFromServerQuery.ts │ │ ├── useGetCountFromServerQuery.test.tsx │ │ ├── useGetCountFromServerQuery.ts │ │ ├── useNamedQuery.test.tsx │ │ ├── useNamedQuery.ts │ │ ├── useRunTransactionMutation.test.tsx │ │ ├── useRunTransactionMutation.ts │ │ ├── useSetDocumentMutation.test.tsx │ │ ├── useSetDocumentMutation.ts │ │ ├── useUpdateDocumentMutation.test.tsx │ │ ├── useUpdateDocumentMutation.ts │ │ ├── useWaitForPendingWritesQuery.test.tsx │ │ ├── useWaitForPendingWritesQuery.ts │ │ ├── useWriteBatchCommitMutation.test.tsx │ │ └── useWriteBatchCommitMutation.ts │ ├── functions │ │ └── index.ts │ ├── index.ts │ ├── installations │ │ └── index.ts │ ├── messaging │ │ └── index.ts │ ├── remote-config │ │ └── index.ts │ └── storage │ │ └── index.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── utils.tsx │ ├── vitest.config.ts │ └── vitest │ └── utils.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": ["example-*"] 11 | } 12 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "demo-project" 4 | }, 5 | "targets": {}, 6 | "etags": {} 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/local-ci.yml: -------------------------------------------------------------------------------- 1 | name: Local CI 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | quality: 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 10 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: "20" 18 | 19 | - name: Enable Corepack 20 | run: corepack enable 21 | 22 | - name: Get pnpm store directory 23 | id: pnpm-cache 24 | shell: bash 25 | run: | 26 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 27 | 28 | - name: Setup pnpm cache 29 | uses: actions/cache@v4 30 | with: 31 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 32 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 33 | restore-keys: | 34 | ${{ runner.os }}-pnpm-store- 35 | 36 | - name: Install dependencies 37 | run: pnpm install 38 | 39 | - name: Run format check 40 | run: pnpm format 41 | 42 | test: 43 | runs-on: ubuntu-latest 44 | timeout-minutes: 30 45 | needs: quality 46 | steps: 47 | - name: Checkout code 48 | uses: actions/checkout@v4 49 | 50 | - name: Setup Node.js 51 | uses: actions/setup-node@v4 52 | with: 53 | node-version: "20" 54 | registry-url: "https://registry.npmjs.org" 55 | 56 | - name: Enable Corepack 57 | run: corepack enable 58 | 59 | - name: Get pnpm store directory 60 | id: pnpm-cache 61 | shell: bash 62 | run: | 63 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 64 | 65 | - name: Setup pnpm cache 66 | uses: actions/cache@v4 67 | with: 68 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 69 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 70 | restore-keys: | 71 | ${{ runner.os }}-pnpm-store- 72 | 73 | - name: Install dependencies 74 | run: pnpm install 75 | 76 | - name: Install Java 77 | run: | 78 | sudo apt-get update 79 | sudo apt-get install -y openjdk-17-jdk 80 | java -version 81 | 82 | - name: Install Firebase CLI 83 | uses: nick-invision/retry@v3 84 | with: 85 | timeout_minutes: 10 86 | retry_wait_seconds: 60 87 | max_attempts: 3 88 | command: npm i -g firebase-tools@14 89 | 90 | # Build packages before testing 91 | - name: Build packages 92 | run: pnpm turbo build 93 | 94 | # Verify build outputs 95 | - name: Verify build outputs 96 | run: | 97 | # Check all packages for dist directories 98 | MISSING_BUILDS="" 99 | for PKG_DIR in packages/*; do 100 | if [ -d "$PKG_DIR" ] && [ -f "$PKG_DIR/package.json" ]; then 101 | PKG_NAME=$(basename "$PKG_DIR") 102 | if [ ! -d "$PKG_DIR/dist" ]; then 103 | MISSING_BUILDS="$MISSING_BUILDS $PKG_NAME" 104 | fi 105 | fi 106 | done 107 | 108 | if [ -n "$MISSING_BUILDS" ]; then 109 | echo "❌ Build outputs not found for: $MISSING_BUILDS" 110 | exit 1 111 | fi 112 | echo "✅ All build outputs verified" 113 | 114 | # Run tests with all emulators (auth, firestore, and data-connect) 115 | - name: Run tests with emulator 116 | run: pnpm test:emulator 117 | -------------------------------------------------------------------------------- /.github/workflows/test-react-firebase-v12.yml: -------------------------------------------------------------------------------- 1 | name: Test React Package with Firebase v12 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | paths: 10 | - "packages/react/**" 11 | - "dataconnect-sdk/**" 12 | - ".github/workflows/test-react-firebase-v12.yml" 13 | 14 | jobs: 15 | test-react-v12: 16 | name: React Package - Firebase v12 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: "20" 27 | 28 | - name: Enable Corepack 29 | run: corepack enable 30 | 31 | - name: Install dependencies 32 | run: pnpm install 33 | 34 | # IMPROVED: Use actions/setup-java instead of apt-get 35 | - name: Setup Java 36 | uses: actions/setup-java@v4 37 | with: 38 | distribution: "temurin" 39 | java-version: "17" 40 | 41 | - name: Install Firebase CLI 42 | uses: nick-invision/retry@v3 43 | with: 44 | timeout_minutes: 10 45 | retry_wait_seconds: 60 46 | max_attempts: 3 47 | command: npm i -g firebase-tools@latest 48 | 49 | - name: Update to Firebase v12 50 | run: | 51 | # Update root devDependencies to Firebase v12 52 | pnpm add -Dw firebase@^12.1.0 53 | 54 | - name: Update Data Connect SDK for Firebase v12 55 | run: | 56 | # Manually update the generated SDK to support Firebase v12 57 | # This is temporary until Firebase CLI officially supports v12 58 | cd dataconnect-sdk/js/default-connector 59 | # Update the peer dependency to include v12 60 | npm pkg set 'peerDependencies.firebase=^12.1.0' 61 | cd ../../.. 62 | 63 | - name: Reinstall dependencies with v12 64 | run: | 65 | # Install with no-frozen-lockfile to allow dependency changes 66 | pnpm install --no-frozen-lockfile 67 | # Force React package to use Firebase v12 68 | cd packages/react 69 | pnpm add -D firebase@^12.1.0 70 | cd ../.. 71 | 72 | - name: Verify Firebase v12 is installed 73 | run: | 74 | cd packages/react 75 | pnpm ls firebase | grep -E "firebase.*12\." || (echo "Firebase v12 not found!" && exit 1) 76 | 77 | - name: Build React package 78 | run: | 79 | cd packages/react 80 | pnpm build 81 | 82 | - name: Run React package tests with emulator 83 | run: | 84 | # Run only React package tests with emulator 85 | firebase emulators:exec --project test-project "cd packages/react && pnpm test:ci" 86 | env: 87 | CI: true 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | coverage 4 | firebase-debug.log 5 | firestore-debug.log 6 | ui-debug.log 7 | dist 8 | *.log 9 | .idea 10 | dataconnect/.dataconnect 11 | 12 | functions/lib/**/*.js 13 | functions/lib/**/*.js.map 14 | 15 | # Firebase cache 16 | .firebase/ 17 | .turbo/ 18 | .next/ 19 | .dataconnect/ 20 | -------------------------------------------------------------------------------- /.opensource/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Query Firebase", 3 | "platforms": ["Web"], 4 | "content": "README.md", 5 | "pages": [], 6 | "related": ["firebase/firebase-js-sdk", "tannerlinsley/react-query"], 7 | "tabs": [] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "name": "vscode-jest-tests.v2", 10 | "request": "launch", 11 | "runtimeExecutable": "yarn", 12 | "args": [ 13 | "test", 14 | "--watch-all=false", 15 | "--test-name-pattern", 16 | "${jest.testNamePattern}", 17 | "--test-path-pattern", 18 | "${jest.testFilePattern}" 19 | ], 20 | "cwd": "${workspaceFolder}/packages/angular", 21 | "console": "integratedTerminal", 22 | "internalConsoleOptions": "neverOpen", 23 | "disableOptimisticBPs": true 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "angular.enable-strict-mode-prompt": false 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

TanStack Query Firebase

2 |

3 | A set of TanStack Query hooks integrating with Firebase. 4 |

5 |

6 | Installation • 7 | Documentation • 8 | License 9 |

10 |
11 | 12 | TanStack Query Firebase provides a set of hooks for handling asynchronous tasks with Firebase in your applications. 13 | 14 | > Looking for React Query Firebase? Check out the [old branch](https://github.com/invertase/tanstack-query-firebase/tree/react-query-firebase). 15 | 16 | ## Why use this library? 17 | 18 | When managing Firebase’s asynchronous API calls within your application, state synchronization can become cumbersome in most applications. You will commonly find yourself handling loading states, error states, and data synchronization manually. 19 | 20 | This library provides a hands-off approach to these problems, by leveraging the popular [TanStack Query](https://tanstack.com/query/latest) project. Out of the box, you get: 21 | 22 | - **Automatic Caching**: Avoid redundant Firebase calls with built-in caching. 23 | - **Out-of-the-box Synchronization**: TanStack Query keeps your UI in sync with the Firebase backend effortlessly. 24 | - **Background Updates**: Fetch and sync data seamlessly in the background without interrupting the user experience. 25 | - **Error Handling & Retries**: Get automatic retries on failed Firebase calls, with robust error handling baked in. 26 | - **Dev Tools for Debugging**: Leverage the React Query Devtools to gain insights into your data-fetching logic and Firebase interactions. 27 | 28 | By combining Firebase with TanStack Query, you can make your app more resilient, performant, and scalable, all while writing less code. 29 | 30 | ## Installation 31 | 32 | This project expects you have `firebase` installed as a peer dependency. If you haven't done so already, install `firebase`: 33 | 34 | ```bash 35 | npm i --save firebase 36 | ``` 37 | 38 | Next, install specific packages for your framework of choice: 39 | 40 | ### React 41 | 42 | ``` 43 | npm i --save @tanstack/react-query @tanstack-query-firebase/react 44 | ``` 45 | 46 | See the [Documentation](https://invertase.docs.page/tanstack-query-firebase/react) for more information on how to use the library. 47 | 48 | ## Status 49 | 50 | The status of the following Firebase services and frameworks are as follows: 51 | 52 | - ✅ Ready for use 53 | - 🟠 Work in progress 54 | - () Not yet started 55 | 56 | | Module | React | Vue | Solid | Angular | Svelte | 57 | |----------------|:------:|:-----:|:-----:|:-------:|:------:| 58 | | analytics | | | | | | 59 | | app-check | | | | | | 60 | | auth | 🟠 | | | | | 61 | | database | | | | | | 62 | | data-connect | ✅ | | | ✅ | | 63 | | firestore | 🟠 | | | | | 64 | | firestore/lite | | | | | | 65 | | functions | | | | | | 66 | | installations | | | | | | 67 | | messaging | | | | | | 68 | | performance | | | | | | 69 | | remote-config | | | | | | 70 | | ai | | | | | | 71 | 72 | ## License 73 | 74 | - See [LICENSE](/LICENSE) 75 | 76 | --- 77 | 78 |

79 | 80 | 81 | 82 |

83 | Built and maintained by Invertase. 84 |

85 |

86 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.1.1/schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "useIgnoreFile": true, 7 | "defaultBranch": "main" 8 | }, 9 | "files": { 10 | "includes": [ 11 | "**", 12 | "!node_modules/**", 13 | "!.pnpm-store/**", 14 | "!dist/**", 15 | "!.next/**", 16 | "!.turbo/**", 17 | "!dataconnect-sdk/**", 18 | "!coverage/**", 19 | "!.dataconnect/**" 20 | ], 21 | "ignoreUnknown": false, 22 | "experimentalScannerIgnores": [ 23 | "dist", 24 | ".next", 25 | ".turbo", 26 | "dataconnect-sdk", 27 | "node_modules", 28 | "coverage", 29 | ".dataconnect" 30 | ] 31 | }, 32 | "formatter": { 33 | "enabled": true, 34 | "indentStyle": "space", 35 | "indentWidth": 2, 36 | "lineWidth": 80, 37 | "lineEnding": "lf", 38 | "attributePosition": "auto" 39 | }, 40 | "linter": { 41 | "enabled": true, 42 | "rules": { 43 | "recommended": true, 44 | "style": { 45 | "noNonNullAssertion": "off", 46 | "useNodejsImportProtocol": "off" 47 | }, 48 | "suspicious": { 49 | "noExplicitAny": "off", 50 | "noConfusingVoidType": "off" 51 | }, 52 | "complexity": { 53 | "noForEach": "off" 54 | }, 55 | "a11y": { 56 | "useButtonType": "off" 57 | } 58 | } 59 | }, 60 | "javascript": { 61 | "formatter": { 62 | "quoteStyle": "double", 63 | "jsxQuoteStyle": "double", 64 | "quoteProperties": "asNeeded", 65 | "trailingCommas": "all", 66 | "semicolons": "always", 67 | "arrowParentheses": "always", 68 | "bracketSpacing": true, 69 | "bracketSameLine": false 70 | } 71 | }, 72 | "json": { 73 | "parser": { 74 | "allowComments": true, 75 | "allowTrailingCommas": false 76 | }, 77 | "formatter": { 78 | "enabled": true, 79 | "indentStyle": "space", 80 | "indentWidth": 2, 81 | "trailingCommas": "none" 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /dataconnect-sdk/js/default-connector/.guides/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "A set of guides for interacting with the generated firebase dataconnect sdk", 3 | "mcpServers": { 4 | "firebase": { 5 | "command": "npx", 6 | "args": ["-y", "firebase-tools@latest", "experimental:mcp"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /dataconnect-sdk/js/default-connector/.guides/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | If the user hasn't already installed the SDK, always run the user's node package manager of choice, and install the package in the directory ../package.json. 4 | For more information on where the library is located, look at the connector.yaml file. 5 | 6 | ```ts 7 | import { initializeApp } from 'firebase/app'; 8 | 9 | initializeApp({ 10 | // fill in your project config here using the values from your Firebase project or from the `firebase_get_sdk_config` tool from the Firebase MCP server. 11 | }); 12 | ``` 13 | 14 | Then, you can run the SDK as needed. 15 | ```ts 16 | import { ... } from '@dataconnect/default-connector'; 17 | ``` 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /dataconnect-sdk/js/default-connector/.guides/usage.md: -------------------------------------------------------------------------------- 1 | # Basic Usage 2 | 3 | Always prioritize using a supported framework over using the generated SDK 4 | directly. Supported frameworks simplify the developer experience and help ensure 5 | best practices are followed. 6 | 7 | 8 | 9 | 10 | 11 | ## Advanced Usage 12 | If a user is not using a supported framework, they can use the generated SDK directly. 13 | 14 | Here's an example of how to use it with the first 5 operations: 15 | 16 | ```js 17 | import { createMovie, upsertMovie, deleteMovie, addMeta, deleteMeta, listMovies, getMovieById, getMeta } from '@dataconnect/default-connector'; 18 | 19 | 20 | // Operation CreateMovie: For variables, look at type CreateMovieVars in ../index.d.ts 21 | const { data } = await CreateMovie(dataConnect, createMovieVars); 22 | 23 | // Operation UpsertMovie: For variables, look at type UpsertMovieVars in ../index.d.ts 24 | const { data } = await UpsertMovie(dataConnect, upsertMovieVars); 25 | 26 | // Operation DeleteMovie: For variables, look at type DeleteMovieVars in ../index.d.ts 27 | const { data } = await DeleteMovie(dataConnect, deleteMovieVars); 28 | 29 | // Operation AddMeta: 30 | const { data } = await AddMeta(dataConnect); 31 | 32 | // Operation DeleteMeta: For variables, look at type DeleteMetaVars in ../index.d.ts 33 | const { data } = await DeleteMeta(dataConnect, deleteMetaVars); 34 | 35 | // Operation ListMovies: 36 | const { data } = await ListMovies(dataConnect); 37 | 38 | // Operation GetMovieById: For variables, look at type GetMovieByIdVars in ../index.d.ts 39 | const { data } = await GetMovieById(dataConnect, getMovieByIdVars); 40 | 41 | // Operation GetMeta: 42 | const { data } = await GetMeta(dataConnect); 43 | 44 | 45 | ``` -------------------------------------------------------------------------------- /dataconnect-sdk/js/default-connector/esm/index.esm.js: -------------------------------------------------------------------------------- 1 | import { queryRef, executeQuery, mutationRef, executeMutation, validateArgs } from 'firebase/data-connect'; 2 | 3 | export const connectorConfig = { 4 | connector: 'default', 5 | service: 'tanstack-query-firebase', 6 | location: 'us-central1' 7 | }; 8 | 9 | export const createMovieRef = (dcOrVars, vars) => { 10 | const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true); 11 | dcInstance._useGeneratedSdk(); 12 | return mutationRef(dcInstance, 'CreateMovie', inputVars); 13 | } 14 | createMovieRef.operationName = 'CreateMovie'; 15 | 16 | export function createMovie(dcOrVars, vars) { 17 | return executeMutation(createMovieRef(dcOrVars, vars)); 18 | } 19 | 20 | export const upsertMovieRef = (dcOrVars, vars) => { 21 | const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true); 22 | dcInstance._useGeneratedSdk(); 23 | return mutationRef(dcInstance, 'UpsertMovie', inputVars); 24 | } 25 | upsertMovieRef.operationName = 'UpsertMovie'; 26 | 27 | export function upsertMovie(dcOrVars, vars) { 28 | return executeMutation(upsertMovieRef(dcOrVars, vars)); 29 | } 30 | 31 | export const deleteMovieRef = (dcOrVars, vars) => { 32 | const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true); 33 | dcInstance._useGeneratedSdk(); 34 | return mutationRef(dcInstance, 'DeleteMovie', inputVars); 35 | } 36 | deleteMovieRef.operationName = 'DeleteMovie'; 37 | 38 | export function deleteMovie(dcOrVars, vars) { 39 | return executeMutation(deleteMovieRef(dcOrVars, vars)); 40 | } 41 | 42 | export const addMetaRef = (dc) => { 43 | const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined); 44 | dcInstance._useGeneratedSdk(); 45 | return mutationRef(dcInstance, 'AddMeta'); 46 | } 47 | addMetaRef.operationName = 'AddMeta'; 48 | 49 | export function addMeta(dc) { 50 | return executeMutation(addMetaRef(dc)); 51 | } 52 | 53 | export const deleteMetaRef = (dcOrVars, vars) => { 54 | const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true); 55 | dcInstance._useGeneratedSdk(); 56 | return mutationRef(dcInstance, 'DeleteMeta', inputVars); 57 | } 58 | deleteMetaRef.operationName = 'DeleteMeta'; 59 | 60 | export function deleteMeta(dcOrVars, vars) { 61 | return executeMutation(deleteMetaRef(dcOrVars, vars)); 62 | } 63 | 64 | export const listMoviesRef = (dc) => { 65 | const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined); 66 | dcInstance._useGeneratedSdk(); 67 | return queryRef(dcInstance, 'ListMovies'); 68 | } 69 | listMoviesRef.operationName = 'ListMovies'; 70 | 71 | export function listMovies(dc) { 72 | return executeQuery(listMoviesRef(dc)); 73 | } 74 | 75 | export const getMovieByIdRef = (dcOrVars, vars) => { 76 | const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true); 77 | dcInstance._useGeneratedSdk(); 78 | return queryRef(dcInstance, 'GetMovieById', inputVars); 79 | } 80 | getMovieByIdRef.operationName = 'GetMovieById'; 81 | 82 | export function getMovieById(dcOrVars, vars) { 83 | return executeQuery(getMovieByIdRef(dcOrVars, vars)); 84 | } 85 | 86 | export const getMetaRef = (dc) => { 87 | const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined); 88 | dcInstance._useGeneratedSdk(); 89 | return queryRef(dcInstance, 'GetMeta'); 90 | } 91 | getMetaRef.operationName = 'GetMeta'; 92 | 93 | export function getMeta(dc) { 94 | return executeQuery(getMetaRef(dc)); 95 | } 96 | 97 | -------------------------------------------------------------------------------- /dataconnect-sdk/js/default-connector/esm/package.json: -------------------------------------------------------------------------------- 1 | {"type":"module"} -------------------------------------------------------------------------------- /dataconnect-sdk/js/default-connector/index.cjs.js: -------------------------------------------------------------------------------- 1 | const { queryRef, executeQuery, mutationRef, executeMutation, validateArgs } = require('firebase/data-connect'); 2 | 3 | const connectorConfig = { 4 | connector: 'default', 5 | service: 'tanstack-query-firebase', 6 | location: 'us-central1' 7 | }; 8 | exports.connectorConfig = connectorConfig; 9 | 10 | const createMovieRef = (dcOrVars, vars) => { 11 | const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true); 12 | dcInstance._useGeneratedSdk(); 13 | return mutationRef(dcInstance, 'CreateMovie', inputVars); 14 | } 15 | createMovieRef.operationName = 'CreateMovie'; 16 | exports.createMovieRef = createMovieRef; 17 | 18 | exports.createMovie = function createMovie(dcOrVars, vars) { 19 | return executeMutation(createMovieRef(dcOrVars, vars)); 20 | }; 21 | 22 | const upsertMovieRef = (dcOrVars, vars) => { 23 | const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true); 24 | dcInstance._useGeneratedSdk(); 25 | return mutationRef(dcInstance, 'UpsertMovie', inputVars); 26 | } 27 | upsertMovieRef.operationName = 'UpsertMovie'; 28 | exports.upsertMovieRef = upsertMovieRef; 29 | 30 | exports.upsertMovie = function upsertMovie(dcOrVars, vars) { 31 | return executeMutation(upsertMovieRef(dcOrVars, vars)); 32 | }; 33 | 34 | const deleteMovieRef = (dcOrVars, vars) => { 35 | const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true); 36 | dcInstance._useGeneratedSdk(); 37 | return mutationRef(dcInstance, 'DeleteMovie', inputVars); 38 | } 39 | deleteMovieRef.operationName = 'DeleteMovie'; 40 | exports.deleteMovieRef = deleteMovieRef; 41 | 42 | exports.deleteMovie = function deleteMovie(dcOrVars, vars) { 43 | return executeMutation(deleteMovieRef(dcOrVars, vars)); 44 | }; 45 | 46 | const addMetaRef = (dc) => { 47 | const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined); 48 | dcInstance._useGeneratedSdk(); 49 | return mutationRef(dcInstance, 'AddMeta'); 50 | } 51 | addMetaRef.operationName = 'AddMeta'; 52 | exports.addMetaRef = addMetaRef; 53 | 54 | exports.addMeta = function addMeta(dc) { 55 | return executeMutation(addMetaRef(dc)); 56 | }; 57 | 58 | const deleteMetaRef = (dcOrVars, vars) => { 59 | const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true); 60 | dcInstance._useGeneratedSdk(); 61 | return mutationRef(dcInstance, 'DeleteMeta', inputVars); 62 | } 63 | deleteMetaRef.operationName = 'DeleteMeta'; 64 | exports.deleteMetaRef = deleteMetaRef; 65 | 66 | exports.deleteMeta = function deleteMeta(dcOrVars, vars) { 67 | return executeMutation(deleteMetaRef(dcOrVars, vars)); 68 | }; 69 | 70 | const listMoviesRef = (dc) => { 71 | const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined); 72 | dcInstance._useGeneratedSdk(); 73 | return queryRef(dcInstance, 'ListMovies'); 74 | } 75 | listMoviesRef.operationName = 'ListMovies'; 76 | exports.listMoviesRef = listMoviesRef; 77 | 78 | exports.listMovies = function listMovies(dc) { 79 | return executeQuery(listMoviesRef(dc)); 80 | }; 81 | 82 | const getMovieByIdRef = (dcOrVars, vars) => { 83 | const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true); 84 | dcInstance._useGeneratedSdk(); 85 | return queryRef(dcInstance, 'GetMovieById', inputVars); 86 | } 87 | getMovieByIdRef.operationName = 'GetMovieById'; 88 | exports.getMovieByIdRef = getMovieByIdRef; 89 | 90 | exports.getMovieById = function getMovieById(dcOrVars, vars) { 91 | return executeQuery(getMovieByIdRef(dcOrVars, vars)); 92 | }; 93 | 94 | const getMetaRef = (dc) => { 95 | const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined); 96 | dcInstance._useGeneratedSdk(); 97 | return queryRef(dcInstance, 'GetMeta'); 98 | } 99 | getMetaRef.operationName = 'GetMeta'; 100 | exports.getMetaRef = getMetaRef; 101 | 102 | exports.getMeta = function getMeta(dc) { 103 | return executeQuery(getMetaRef(dc)); 104 | }; 105 | -------------------------------------------------------------------------------- /dataconnect-sdk/js/default-connector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dataconnect/default-connector", 3 | "version": "1.0.0", 4 | "author": "Firebase (https://firebase.google.com/)", 5 | "description": "Generated SDK For default", 6 | "license": "Apache-2.0", 7 | "engines": { 8 | "node": " >=18.0" 9 | }, 10 | "typings": "index.d.ts", 11 | "module": "esm/index.esm.js", 12 | "main": "index.cjs.js", 13 | "browser": "esm/index.esm.js", 14 | "exports": { 15 | ".": { 16 | "types": "./index.d.ts", 17 | "require": "./index.cjs.js", 18 | "default": "./esm/index.esm.js" 19 | }, 20 | "./package.json": "./package.json" 21 | }, 22 | "peerDependencies": { 23 | "firebase": "^10.14.0 || ^11.3.0 || ^12.0.0" 24 | } 25 | } -------------------------------------------------------------------------------- /dataconnect/connector/connector.yaml: -------------------------------------------------------------------------------- 1 | connectorId: default 2 | generate: 3 | javascriptSdk: 4 | outputDir: ../../dataconnect-sdk/js/default-connector 5 | package: "@dataconnect/default-connector" 6 | -------------------------------------------------------------------------------- /dataconnect/connector/mutations.gql: -------------------------------------------------------------------------------- 1 | # # Example mutations for a simple movie app 2 | 3 | # Create a movie based on user input 4 | mutation CreateMovie($title: String!, $genre: String!, $imageUrl: String!) 5 | @auth(level: PUBLIC) { 6 | movie_insert(data: { title: $title, genre: $genre, imageUrl: $imageUrl }) 7 | } 8 | 9 | # Upsert a movie 10 | mutation UpsertMovie($id: UUID!, $title: String!, $imageUrl: String!) 11 | @auth(level: PUBLIC) { 12 | movie_upsert(data: { id: $id, title: $title, imageUrl: $imageUrl }) 13 | } 14 | 15 | # Delete a movie 16 | mutation DeleteMovie($id: UUID!) @auth(level: PUBLIC) { 17 | movie_delete(id: $id) 18 | } 19 | 20 | mutation AddMeta @auth(level: PUBLIC) { 21 | ref: meta_insert(data: { ref: "" }) 22 | } 23 | mutation DeleteMeta($id: UUID!) @auth(level: PUBLIC) { 24 | ref: meta_delete(id: $id) 25 | } 26 | 27 | # # Upsert (update or insert) a user's username based on their auth.uid 28 | # mutation UpsertUser($username: String!) @auth(level: USER) { 29 | # user_upsert( 30 | # data: { 31 | # id_expr: "auth.uid" 32 | # username: $username 33 | # } 34 | # ) 35 | # } 36 | 37 | # # Add a review for a movie 38 | # mutation AddReview( 39 | # $movieId: UUID! 40 | # $rating: Int! 41 | # $reviewText: String! 42 | # ) @auth(level: USER) { 43 | # review_upsert( 44 | # data: { 45 | # userId_expr: "auth.uid" 46 | # movieId: $movieId 47 | # rating: $rating 48 | # reviewText: $reviewText 49 | # # reviewDate defaults to today in the schema. No need to set it manually. 50 | # } 51 | # ) 52 | # } 53 | 54 | # # Logged in user can delete their review for a movie 55 | # mutation DeleteReview( 56 | # $movieId: UUID! 57 | # ) @auth(level: USER) { 58 | # review_delete(key: { userId_expr: "auth.uid", movieId: $movieId }) 59 | # } 60 | -------------------------------------------------------------------------------- /dataconnect/connector/queries.gql: -------------------------------------------------------------------------------- 1 | # # Example queries for a simple movie app. 2 | 3 | # @auth() directives control who can call each operation. 4 | # Anyone should be able to list all movies, so the auth level is set to PUBLIC 5 | query ListMovies @auth(level: PUBLIC) { 6 | movies { 7 | id 8 | title 9 | imageUrl 10 | genre 11 | } 12 | } 13 | 14 | # Get movie by id 15 | query GetMovieById($id: UUID!) @auth(level: PUBLIC) { 16 | movie(id: $id) { 17 | id 18 | title 19 | imageUrl 20 | genre 21 | } 22 | } 23 | 24 | query GetMeta @auth(level: PUBLIC) { 25 | ref: metas { 26 | id 27 | } 28 | } 29 | 30 | # # List all users, only admins should be able to list all users, so we use NO_ACCESS 31 | # query ListUsers @auth(level: NO_ACCESS) { 32 | # users { id, username } 33 | # } 34 | 35 | # # Logged in user can list all their reviews and movie titles associated with the review 36 | # # Since the query requires the uid of the current authenticated user, the auth level is set to USER 37 | # query ListUserReviews @auth(level: USER) { 38 | # user(key: {id_expr: "auth.uid"}) { 39 | # id 40 | # username 41 | # # _on_ makes it easy to grab info from another table 42 | # # Here, we use it to grab all the reviews written by the user. 43 | # reviews: reviews_on_user { 44 | # id 45 | # rating 46 | # reviewDate 47 | # reviewText 48 | # movie { 49 | # id 50 | # title 51 | # } 52 | # } 53 | # } 54 | # } 55 | 56 | # # Search for movies, actors, and reviews 57 | # query SearchMovie( 58 | # $titleInput: String 59 | # $genre: String 60 | # ) @auth(level: PUBLIC) { 61 | # movies( 62 | # where: { 63 | # _and: [ 64 | # { genre: { eq: $genre } } 65 | # { title: { contains: $titleInput } } 66 | # ] 67 | # } 68 | # ) { 69 | # id 70 | # title 71 | # genre 72 | # imageUrl 73 | # } 74 | # } 75 | -------------------------------------------------------------------------------- /dataconnect/dataconnect.yaml: -------------------------------------------------------------------------------- 1 | specVersion: "v1beta" 2 | serviceId: "tanstack-query-firebase" 3 | location: "us-central1" 4 | schema: 5 | source: "./schema" 6 | datasource: 7 | postgresql: 8 | database: "fdcdb" 9 | cloudSql: 10 | instanceId: "tanstack-query-firebase-fdc" 11 | # schemaValidation: "COMPATIBLE" 12 | connectorDirs: ["./connector"] 13 | -------------------------------------------------------------------------------- /dataconnect/schema/schema.gql: -------------------------------------------------------------------------------- 1 | # # Example schema for simple movie review app 2 | 3 | # # Users 4 | # # Suppose a user can leave reviews for movies 5 | # # user -> reviews is a one to many relationship, 6 | # # movie -> reviews is a one to many relationship 7 | # # movie <-> user is a many to many relationship 8 | # type User @table { 9 | # id: String! @col(name: "user_auth") 10 | # username: String! @col(name: "username", dataType: "varchar(50)") 11 | # # The following are generated by the user: User! field in the Review table 12 | # # reviews_on_user 13 | # # movies_via_Review 14 | # } 15 | 16 | # Movies 17 | type Movie @table { 18 | # The below parameter values are generated by default with @table, and can be edited manually. 19 | # implies directive `@col(name: "movie_id")`, generating a column name 20 | id: UUID! @default(expr: "uuidV4()") 21 | title: String! 22 | imageUrl: String! 23 | genre: String 24 | } 25 | 26 | # Movie Metadata 27 | # Movie - MovieMetadata is a one-to-one relationship 28 | type MovieMetadata @table { 29 | # @unique indicates a 1-1 relationship 30 | movie: Movie! @unique 31 | # movieId: UUID <- this is created by the above reference 32 | rating: Float 33 | releaseYear: Int 34 | description: String 35 | } 36 | 37 | type Meta @table { 38 | ref: String! 39 | } 40 | 41 | # # Reviews 42 | # type Review @table(name: "Reviews", key: ["movie", "user"]) { 43 | # id: UUID! @default(expr: "uuidV4()") 44 | # user: User! 45 | # movie: Movie! 46 | # rating: Int 47 | # reviewText: String 48 | # reviewDate: Date! @default(expr: "request.time") 49 | # } 50 | -------------------------------------------------------------------------------- /docs/angular/data-connect/functions/injectDataConnectMutation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: injectDataConnectMutation 3 | --- 4 | 5 | `injectDataConnectMutation` is an injector designed to simplify handling mutations (creating, updating, deleting) with Firebase Data Connect. 6 | 7 | See [mutations](/angular/data-connect/mutations) for more information. 8 | 9 | ## Features 10 | 11 | - Simplifies mutation handling for create, update, and delete operations using Firebase Data Connect. 12 | - Provides type-safe handling of mutations based on your Firebase Data Connect schema. 13 | - Automatically manages pending, success, and error states for mutations. 14 | - Supports optimistic updates and caching to improve user experience and performance. 15 | 16 | ## Usage 17 | 18 | ```ts 19 | import { injectDataConnectMutation } from "@tanstack-query-firebase/angular/data-connect"; 20 | import { createMovieRef } from "@your-package-name/your-connector"; 21 | 22 | class AddMovieComponent { 23 | createMovie = injectDataConnectMutation( 24 | createMovieRef 25 | ); 26 | addMovie() { 27 | createMovie.mutate({ 28 | title: 'John Wick', 29 | genre: "Action", 30 | imageUrl: "https://example.com/image.jpg", 31 | }); 32 | } 33 | return ( 34 | 40 | ); 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/angular/data-connect/functions/injectDataConnectQuery.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: injectDataConnectQuery 3 | --- 4 | 5 | `injectDataConnectQuery` is an injector designed to simplify data fetching and state management with Firebase Data Connect. 6 | 7 | See [querying](/angular/data-connect/querying) for more information. 8 | 9 | ## Features 10 | 11 | - Provides type-safe handling of queries based on the Firebase Data Connect schema. 12 | - Simplifies data fetching using Firebase Data Connect. 13 | - Automatically manages loading, success, and error states. 14 | - Supports refetching data with integrated caching. 15 | 16 | ## Usage 17 | 18 | ```ts 19 | import { injectDataConnectQuery } from '@tanstack-query-firebase/angular/data-connect'; 20 | import { listMoviesRef } from "@your-package-name/your-connector"; 21 | 22 | // class 23 | export class MovieListComponent { 24 | movies = injectDataConnectQuery(listMoviesRef()); 25 | } 26 | 27 | // template 28 | @if (movies.isPending()) { 29 | Loading... 30 | } 31 | @if (movies.error()) { 32 | An error has occurred: {{ movies.error() }} 33 | } 34 | @if (movies.data(); as data) { 35 | @for (movie of data.movies; track movie.id) { 36 | 37 | {{movie.description}} 38 | 39 | } @empty { 40 |

No items!

41 | } 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/angular/data-connect/mutations.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mutations 3 | description: Learn how to mutate data in Firebase Data Connect using the Tanstack Query Firebase injectors. 4 | --- 5 | 6 | ## Mutating Data 7 | 8 | To mutate data in Firebase Data Connect, you can either use the generated injectors, or use the `injectDataConnectMutation` injector. 9 | 10 | ```ts 11 | import { injectCreateMovie } from "@firebasegen/movies/angular"; 12 | 13 | @Component({ 14 | ... 15 | template: ` 16 | 22 | ` 23 | }) 24 | class AddMovieComponent() { 25 | // Calls `injectDataConnectMutation` with the respective types. 26 | // Alternatively: 27 | // import { injectDataConnectMutation } from '@tanstack-query-firebase/angular/data-connect'; 28 | // ... 29 | // createMovie = injectDataConnectMutation(createMovieRef); 30 | createMovie = injectCreateMovie(); 31 | addMovie() { 32 | createMovie.mutate({ 33 | title: 'John Wick', 34 | genre: "Action", 35 | imageUrl: "https://example.com/image.jpg", 36 | }); 37 | } 38 | } 39 | ``` 40 | 41 | Additionally, you can provide a factory function to the mutation, which will be called with the mutation variables: 42 | 43 | ```ts 44 | createMovie = injectDataConnectMutation(undefined, () => ({ 45 | mutationFn: (title: string) => createMovieRef({ title, reviewDate: Date.now() }) 46 | })); 47 | 48 | // ... 49 | createMovie.mutate("John Wick"); 50 | ``` 51 | 52 | ## Invalidating Queries 53 | 54 | The function provides an additional [mutation option](https://tanstack.com/query/latest/docs/framework/angular/reference/functions/injectMutation) called `invalidate`. This option accepts a list of query references which will be automatically invalidated when the mutation is successful. 55 | 56 | You can also provide explicit references to the invalidate array, for example: 57 | 58 | ```ts 59 | const createMovie = injectDataConnectMutation(createMovieRef, { 60 | invalidate: [getMovieRef({ id: "1" })], 61 | }); 62 | ``` 63 | 64 | In this case only the query reference `getMovieRef({ id: "1" })` will be invalidated. 65 | 66 | ## Overriding the mutation key 67 | 68 | ### Metadata 69 | 70 | Along with the data, the function will also return the `ref`, `source`, and `fetchTime` metadata from the mutation. 71 | 72 | ```ts 73 | const createMovie = injectDataConnectMutation(createMovieRef); 74 | 75 | await createMovie.mutateAsync({ 76 | title: 'John Wick', 77 | genre: "Action", 78 | imageUrl: "https://example.com/image.jpg", 79 | }); 80 | 81 | console.log(createMovie.dataConnectResult().ref); 82 | console.log(createMovie.dataConnectResult().source); 83 | console.log(createMovie.dataConnectResult().fetchTime); 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/angular/data-connect/querying.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Querying 3 | description: Learn how to query data from Firebase Data Connect using the Tanstack Query Firebase injectors. 4 | --- 5 | 6 | ## Querying Data 7 | 8 | To query data from Firebase Data Connect, you can either use the generated injectors, or the `injectDataConnect` injector. This will automatically create a query key and infer the data type and variables associated with the query. 9 | 10 | ```ts 11 | import { injectListMyPosts } from '@firebasegen/posts/angular' 12 | 13 | @Component({ 14 | ... 15 | template: ` 16 | @if (movies.isPending()) { 17 | Loading... 18 | } 19 | @if (movies.error()) { 20 | An error has occurred: {{ movies.error() }} 21 | } 22 | @if (movies.data(); as data) { 23 | @for (movie of data.movies; track movie.id) { 24 | 25 | {{movie.description}} 26 | 27 | } @empty { 28 |

No items!

29 | } 30 | } 31 | `, 32 | }) 33 | export class PostListComponent { 34 | // Calls `injectDataConnectQuery` with the respective types. 35 | // Alternatively: 36 | // import { injectDataConnectQuery } from '@tanstack-query-firebase/angular/data-connect'; 37 | // ... 38 | // injectDataConnectQuery(listMoviesRef()) 39 | movies = injectListMovies(); 40 | } 41 | ``` 42 | 43 | ### Query Options 44 | 45 | To leverage the full power of Tanstack Query, you can pass in query options to the `injectDataConnectQuery` injector, for example to refetch the query on a interval: 46 | 47 | ```ts 48 | movies = injectListMovies( 49 | { 50 | refetchInterval: 1000, 51 | } 52 | ); 53 | ``` 54 | The injector extends the [`injectQuery`](https://tanstack.com/query/latest/docs/framework/angular/reference/functions/injectquery) injector, so you can learn more about the available options by reading the [Tanstack Query documentation](https://tanstack.com/query/latest/docs/framework/angular/reference/functions/injectquery). 55 | 56 | ### Overriding the query key 57 | 58 | To override the query key, you can pass in a custom query key to the `injectDataConnectQuery` injector: 59 | 60 | ```ts 61 | movies = injectListMovies( 62 | listMoviesRef(), 63 | { 64 | queryKey: ['movies', '1'] 65 | } 66 | ); 67 | ``` 68 | Note that overriding the query key could mean your query is no longer synchronized with mutation invalidations or server side rendering pre-fetching. 69 | 70 | ### Metadata 71 | 72 | Along with the data, the injector will also return the `ref`, `source`, and `fetchTime` metadata from the query. 73 | 74 | ```ts 75 | const movies = injectListMovies(); 76 | 77 | console.log(movies.dataConnectResult()?.ref); 78 | console.log(movies.dataConnectResult()?.source); 79 | console.log(movies.dataConnectResult()?.fetchTime); 80 | ``` 81 | 82 | -------------------------------------------------------------------------------- /docs/angular/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Angular 3 | description: Using TanStack Query Firebase with Angular 4 | --- 5 | 6 | To get started using TanStack Query Firebase with Angular, you will need to install the following packages: 7 | 8 | ```bash 9 | npm i --save firebase @tanstack/angular-query-experimental @tanstack-query-firebase/angular 10 | ``` 11 | 12 | Both `@angular/fire` and `@tanstack/angular-query-experimental` are peer dependencies of `@tanstack-query-firebase/angular`. 13 | 14 | ## Usage 15 | 16 | TanStack Query Firebase provides a hands-off approach to integrate with TanStack Query - you are 17 | still responsible for both setting up Firebase in your application and configuring TanStack Query. 18 | 19 | If you haven't already done so, [initialize your Firebase project](https://firebase.google.com/docs/web/setup) 20 | and [configure TanStack Query](https://tanstack.com/query/latest/docs/framework/angular/quick-start): 21 | 22 | ### Automatic Setup 23 | To automatically set up AngularFire, just run: 24 | ```shell 25 | ng add @angular/fire 26 | ``` 27 | 28 | ### Manual Setup 29 | 30 | ```ts 31 | // app.config.ts 32 | import { initializeApp, provideFirebaseApp } from '@angular/fire/app'; 33 | import { getDataConnect, provideDataConnect } from '@angular/fire/data-connect'; 34 | import { connectorConfig } from '@firebasegen/movies'; 35 | import { provideTanStackQuery, QueryClient } from '@tanstack/angular-query-experimental'; 36 | 37 | 38 | export const appConfig: ApplicationConfig = { 39 | providers: [ 40 | ... 41 | provideFirebaseApp(() => 42 | initializeApp(/*Replace with your firebase config*/) 43 | ), 44 | provideDataConnect(() => getDataConnect(connectorConfig)), 45 | provideTanStackQuery(new QueryClient()), 46 | ], 47 | }; 48 | ``` 49 | 50 | And be sure to add `angular: true` to your `connector.yaml`: 51 | 52 | ```yaml 53 | generate: 54 | javascriptSdk: 55 | angular: true 56 | outputDir: "../movies-generated" 57 | package: "@movie-app/movies" 58 | packageJsonDir: "../../" 59 | ``` 60 | 61 | ## Example Usage 62 | 63 | Next, you can start to use injectors provided by `@tanstack-query-firebase/angular`. For example, to 64 | fetch a query from Data Connect: 65 | 66 | ```ts 67 | import { injectListMovies } from '@firebasegen/movies/angular' 68 | 69 | // class 70 | export class MovieListComponent { 71 | movies = injectListMovies(); 72 | } 73 | 74 | // template 75 | @if (movies.isPending()) { 76 | Loading... 77 | } 78 | @if (movies.error()) { 79 | An error has occurred: {{ movies.error() }} 80 | } 81 | @if (movies.data(); as data) { 82 | @for (movie of data.movies; track movie.id) { 83 | 84 | {{movie.description}} 85 | 86 | } @empty { 87 |

No items!

88 | } 89 | } 90 | ``` 91 | -------------------------------------------------------------------------------- /docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: TanStack Query Firebase 3 | description: TanStack Query Firebase provides a set of hooks for handling asynchronous tasks with Firebase in your applications. 4 | --- 5 | 6 | When managing Firebase's asynchronous API calls within your application, state synchronization can become cumbersome in most applications. You will commonly find yourself handling loading states, error states, and data synchronization manually. 7 | 8 | This library provides a hands-off approach to these problems, by leveraging the popular [TanStack Query](https://tanstack.com/query/latest) project. Out of the box, you get: 9 | 10 | - **Automatic Caching**: Avoid redundant Firebase calls with built-in caching. 11 | - **Out-of-the-box Synchronization**: TanStack Query keeps your UI in sync with the Firebase backend effortlessly. 12 | - **Background Updates**: Fetch and sync data seamlessly in the background without interrupting the user experience. 13 | - **Error Handling & Retries**: Get automatic retries on failed Firebase calls, with robust error handling baked in. 14 | - **Dev Tools for Debugging**: Leverage the React Query Devtools to gain insights into your data-fetching logic and Firebase interactions. 15 | 16 | By combining Firebase with TanStack Query, you can make your app more resilient, performant, and scalable, all while writing less code. 17 | 18 | Looking for React Query Firebase? Check out the [old branch](https://github.com/invertase/tanstack-query-firebase/tree/react-query-firebase). 19 | 20 | ## Framework Installation 21 | 22 | This project aims to support hooks for all major frameworks which TanStack supports; React, Vue, Solid, Angular, and Svelte. Development is still in progress, with an initial focus on React. 23 | 24 | To get started with your framework of choice, view the following documentation: 25 | 26 | - [React Documentation](/react) 27 | 28 | -------------------------------------------------------------------------------- /docs/react-query-firebase.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Migrating to TanStack Query Firebase 3 | description: Migrating from the old React Query Firebase to the new TanStack Query Firebase. 4 | --- 5 | 6 | The initial version of this project was called `React Query Firebase`, and built upon the 7 | older versions of *React Query*. Over the past couple of years, there's been many changes 8 | to the React Query library. 9 | 10 | The most substantial change was renaming the libray from *React Query* to *TanStack Query*. 11 | 12 | The change brought about support for a wide array of framework support beyond React, including 13 | Vue, Solid, Angular, and Svelte. The API has also evolved during this time, with many improvements 14 | and new features. 15 | 16 | The Firebase API also evolved during this time, with new services such as Data Connect and the migration 17 | from the compat API to the modular API. 18 | 19 | ## react-query-firebase 20 | 21 | The `react-query-firebase` package was built to support React only, and was tightly coupled to 22 | the older versions of React Query. For example, the `react-query-firebase` NPN namespace allowed you 23 | to install a package per Firebase service, such as `@react-query-firebase/firestore`. 24 | 25 | Additionally, the API was designed to work with the older React Query API of supporting positional args 26 | vs the newer object-based API: 27 | 28 | ```tsx 29 | useFirestoreQuery(["products"]); 30 | // vs 31 | useFirestoreQuery({ queryKey: ["products"] }); 32 | ``` 33 | 34 | ## tanstack-query-firebase 35 | 36 | The `tanstack-query-firebase` package is built to support all frameworks which TanStack Query supports, 37 | although initially only React is supported. 38 | 39 | Altough still in development, the API is designed to work with the newer object-based API of TanStack Query, 40 | and also supports newer Firebase services such as Data Connect. 41 | 42 | ### Realtime Subscription Issues 43 | 44 | Firebase supports realtime event subscriptions for many of its services, such as Firestore, Realtime Database and 45 | Authentication. 46 | 47 | The `react-query-firebase` package had a [limitation](https://github.com/invertase/tanstack-query-firebase/issues/25) whereby the hooks 48 | would not resubscribe whenever a component re-mounted. 49 | 50 | The initial version of `tanstack-query-firebase` currently opts-out of any realtime subscription hooks. This issue will be re-addressed 51 | once the core API is stable supporting all Firebase services. 52 | 53 | ## Migration Steps 54 | 55 | Follow the steps below to migrate your application from `react-query-firebase` to `tanstack-query-firebase`: 56 | 57 | ### 1. Install the new packages 58 | 59 | Due to the restructure of the package namespace, you will need to install the new packages: 60 | 61 | ```bash 62 | npm i --save firebase @tanstack/react-query @tanstack-query-firebase/react 63 | ``` 64 | 65 | Remove any existing `@react-query-firebase/*` packages from your `package.json`. 66 | 67 | ### 2. Update your imports 68 | 69 | Update any imports for your `react-query-firebase` hooks to the new `tanstack-query-firebase` hooks, for example for Firestore: 70 | 71 | ```diff 72 | - import { useFirestoreDocument } from '@react-query-firebase/firestore'; 73 | + import { useDocumentQuery } from '@tanstack-query-firebase/react/firestore'; 74 | ``` 75 | 76 | ### 3. Update your usage 77 | 78 | The older API followed the positional args pattern, whereas the new API follows the object-based pattern. Update your hooks to use the new pattern: 79 | 80 | ```diff 81 | - useFirestoreDocument(["products"], ref); 82 | + useDocumentQuery(ref, { 83 | + queryKey: ["products"], 84 | + }); 85 | ``` 86 | -------------------------------------------------------------------------------- /docs/react/auth/hooks/useDeleteUserMutation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useDeleteUserMutation 3 | --- 4 | 5 | This hook is implemented, but the documentation is a work in progress, check back soon. -------------------------------------------------------------------------------- /docs/react/auth/hooks/useReloadMutation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useReloadMutation 3 | --- 4 | 5 | This hook is implemented, but the documentation is a work in progress, check back soon. -------------------------------------------------------------------------------- /docs/react/auth/hooks/useSendSignInLinkToEmailMutation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useSendSignInLinkToEmailMutation 3 | --- 4 | 5 | Send a sign-in link to a user's email address. 6 | 7 | ## Usage 8 | 9 | ```jsx 10 | import { useSendSignInLinkToEmailMutation } from "@tanstack-query-firebase/react/auth"; 11 | import { auth } from "../firebase"; 12 | 13 | function Component() { 14 | const mutation = useSendSignInLinkToEmailMutation(auth, { 15 | onSuccess: () => { 16 | console.log("Sign-in link sent successfully!"); 17 | }, 18 | }); 19 | 20 | return ( 21 | 27 | ); 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/react/auth/hooks/useSignInAnonymouslyMutation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useSignInAnonymouslyMutation 3 | --- 4 | 5 | This hook is implemented, but the documentation is a work in progress, check back soon. -------------------------------------------------------------------------------- /docs/react/auth/hooks/useSignInWithCredentialMutation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useSignInWithCredentialMutation 3 | --- 4 | 5 | This hook is implemented, but the documentation is a work in progress, check back soon. -------------------------------------------------------------------------------- /docs/react/auth/hooks/useSignInWithEmailAndPasswordMutation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useSignInWithEmailAndPasswordMutation 3 | --- 4 | 5 | This hook is implemented, but the documentation is a work in progress, check back soon. -------------------------------------------------------------------------------- /docs/react/auth/hooks/useSignOutMutation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useSignOutMutation 3 | --- 4 | 5 | This hook is implemented, but the documentation is a work in progress, check back soon. -------------------------------------------------------------------------------- /docs/react/auth/hooks/useUpdateCurrentUserMutation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useUpdateCurrentUserMutation 3 | --- 4 | 5 | This hook is implemented, but the documentation is a work in progress, check back soon. -------------------------------------------------------------------------------- /docs/react/auth/hooks/useVerifyPasswordResetCodeMutation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useVerifyPasswordResetCodeMutation 3 | --- 4 | 5 | This hook is implemented, but the documentation is a work in progress, check back soon. -------------------------------------------------------------------------------- /docs/react/auth/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Firebase Authentication 3 | --- 4 | 5 | ## Setup 6 | 7 | Before using the Tanstack Query Firebase hooks for Authentication, ensure you have configured your Firebase application 8 | to setup an Auth instance: 9 | 10 | ```ts 11 | import { initializeApp } from "firebase/app"; 12 | import { getAuth } from "firebase/auth"; 13 | 14 | // Initialize your Firebase app 15 | initializeApp({ ... }); 16 | 17 | // Get the Auth instance 18 | const auth = getAuth(app); 19 | ``` 20 | 21 | ## Importing 22 | 23 | The package exports are available via the `@tanstack-query-firebase/react` package under the `auth` namespace: 24 | 25 | ```ts 26 | import { useSignOutMutation } from "@tanstack-query-firebase/react/auth"; 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/react/data-connect/hooks/useDataConnectMutation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useDataConnectMutation 3 | --- 4 | 5 | `useDataConnectMutation` is a hook designed to simplify handling mutations (creating, updating, deleting) with Firebase Data Connect. 6 | 7 | See [mutations](/react/data-connect/mutations) for more information. 8 | 9 | ## Features 10 | 11 | - Simplifies mutation handling for create, update, and delete operations using Firebase Data Connect. 12 | - Provides type-safe handling of mutations based on your Firebase Data Connect schema. 13 | - Automatically manages pending, success, and error states for mutations. 14 | - Supports optimistic updates and caching to improve user experience and performance. 15 | 16 | ## Usage 17 | 18 | ```jsx 19 | import { useDataConnectQuery } from "@tanstack-query-firebase/react/data-connect"; 20 | import { createMovieRef } from "@your-package-name/your-connector"; 21 | 22 | function Component() { 23 | const { mutate, isPending, isSuccess, isError, error } = 24 | useDataConnectMutation(createMovieRef); 25 | 26 | const handleFormSubmit = (e: React.FormEvent) => { 27 | e.preventDefault(); 28 | const data = new FormData(e.target as HTMLFormElement); 29 | 30 | mutate({ 31 | title: data.get("title") as string, 32 | imageUrl: data.get("imageUrl") as string, 33 | genre: data.get("genre") as string, 34 | }); 35 | }; 36 | 37 | if (isPending) return
Adding movie...
; 38 | 39 | if (isError) return
Error: {error.message}
; 40 | 41 | return ( 42 |
43 | {isSuccess &&
Movie added successfully!
} 44 |
45 | 46 | 47 | 48 | 49 | {/* Form fields for movie data */} 50 | 53 |
54 |
55 | ); 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/react/data-connect/hooks/useDataConnectQuery.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useDataConnectQuery 3 | --- 4 | 5 | `useDataConnectQuery` is a hook designed to simplify data fetching and state management with Firebase Data Connect. 6 | 7 | See [querying](/react/data-connect/querying) for more information. 8 | 9 | ## Features 10 | 11 | - Provides type-safe handling of queries based on the Firebase Data Connect schema. 12 | - Simplifies data fetching using Firebase Data Connect. 13 | - Automatically manages loading, success, and error states. 14 | - Supports refetching data with integrated caching. 15 | 16 | ## Usage 17 | 18 | ```jsx 19 | import { useDataConnectQuery } from "@tanstack-query-firebase/react/data-connect"; 20 | import { listMoviesQuery } from "@your-package-name/your-connector"; 21 | 22 | function Component() { 23 | const { data, isPending, isSuccess, isError, error } = useDataConnectQuery( 24 | listMoviesQuery() 25 | ); 26 | 27 | if (isPending) return
Loading...
; 28 | if (isError) return
Error: {error.message}
; 29 | 30 | return ( 31 |
32 | {isSuccess && ( 33 |
    34 | {data.movies.map((movie) => ( 35 |
  • {movie.title}
  • 36 | ))} 37 |
38 | )} 39 |
40 | ); 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/react/data-connect/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Firebase Data Connect 3 | --- 4 | 5 | Firebase Data Connect is a relational database service for mobile and web apps that lets you build and scale using a fully-managed PostgreSQL database powered by Cloud SQL. It provides secure schema, query and mutation management using GraphQL technology that integrates well with Firebase Authentication. 6 | 7 | To get started, ensure you have setup your Firebase project and have the Data Connect setup in your project. To learn more, 8 | follow the [Firebase Data Connect documentation](https://firebase.google.com/docs/data-connect/quickstart). 9 | 10 | ## Setup 11 | 12 | Before using the Tanstack Query Firebase hooks for Data Connect, ensure you have configured your application using your chosen connector: 13 | 14 | ```ts 15 | import { connectorConfig } from "../../dataconnect/default-connector"; 16 | import { initializeApp } from "firebase/app"; 17 | import { getDataConnect } from "firebase/data-connect"; 18 | 19 | // Initialize your Firebase app 20 | initializeApp({ ... }); 21 | 22 | // Get the Data Connect instance 23 | const dataConnect = getDataConnect(connectorConfig); 24 | 25 | // Optionally, connect to the Data Connect Emulator 26 | connectDataConnectEmulator(dataConnect, "localhost", 9399); 27 | ``` 28 | 29 | ## Importing 30 | 31 | The package exports are available via the `@tanstack-query-firebase/react` package under the `data-connect` namespace: 32 | 33 | ```ts 34 | import { useDataConnectQuery } from "@tanstack-query-firebase/react/data-connect"; 35 | ``` 36 | 37 | ## Basic Usage 38 | 39 | To use the Tanstack Query Firebase hooks for Data Connect, you can use the `useDataConnectQuery` hook to fetch data from the database: 40 | 41 | ```tsx 42 | import { useDataConnectQuery } from "@tanstack-query-firebase/react/data-connect"; 43 | import { listMoviesRef } from "../../dataconnect/default-connector"; 44 | 45 | function Component() { 46 | const { data, isPending, isSuccess, isError, error } = useDataConnectQuery( 47 | listMoviesRef() 48 | ); 49 | 50 | if (isPending) return
Loading...
; 51 | 52 | if (isError) return
Error: {error.message}
; 53 | 54 | return
{isSuccess &&
    {data.movies.map((movie) =>
  • {movie.title}
  • )}
}
; 55 | } 56 | ``` 57 | 58 | The hooks will automatically infer the data type from the connector and the query and automatically create a [query key](https://tanstack.com/query/latest/docs/framework/react/guides/query-keys) for the query. 59 | 60 | ## Learning more 61 | 62 | To learn more about the Data Connect hooks, check out the following pages: 63 | 64 | - [Querying](/react/data-connect/querying) 65 | - [Mutations](/react/data-connect/mutations) 66 | - [Server Side Rendering](/react/data-connect/server-side-rendering) 67 | 68 | -------------------------------------------------------------------------------- /docs/react/data-connect/mutations.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mutations 3 | description: Learn how to mutate data in Firebase Data Connect using the Tanstack Query Firebase hooks. 4 | --- 5 | 6 | ## Mutating Data 7 | 8 | To mutate data in Firebase Data Connect, you can use the `useDataConnectMutation` hook. 9 | 10 | ```tsx 11 | import { useDataConnectMutation } from "@tanstack-query-firebase/react/data-connect"; 12 | import { createMovieRef } from "@dataconnect/default-connector"; 13 | 14 | function Component() { 15 | const createMovie = useDataConnectMutation( 16 | createMovieRef 17 | ); 18 | 19 | return ( 20 | 32 | ); 33 | } 34 | ``` 35 | 36 | Additionally, you can provide a factory function to the mutation, which will be called with the mutation variables: 37 | 38 | ```tsx 39 | const createMovie = useDataConnectMutation((title: string) => createMovieRef({ title, reviewDate: Date.now() })); 40 | // ... 41 | createMovie.mutate("John Wick"); 42 | ``` 43 | 44 | ## Invalidating Queries 45 | 46 | The hook provides an additional [mutation option](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation) called `invalidate`. This option accepts a list of query references which will be automatically invalidated when the mutation is successful. 47 | 48 | ```tsx 49 | const createMovie = useDataConnectMutation(createMovieRef, { 50 | invalidate: [getMovieRef], 51 | }); 52 | ``` 53 | 54 | ### Implicit references 55 | 56 | The above example provides a `getMovieRef` instance to the invalidate array. By default this will invalidate all queries that cached via the `getMovieRef` reference, for example the following query references will be invalidated: 57 | 58 | ```tsx 59 | getMovieRef({ id: "1"}); 60 | getMovieRef({ id: "2"}); 61 | ``` 62 | 63 | ### Explicit references 64 | 65 | You can also provide explicit references to the invalidate array, for example: 66 | 67 | ```tsx 68 | const createMovie = useDataConnectMutation(createMovieRef, { 69 | invalidate: [getMovieRef({ id: "1" })], 70 | }); 71 | ``` 72 | 73 | In this case only the query reference `getMovieRef({ id: "1" })` will be invalidated. 74 | 75 | ## Overriding the mutation key 76 | 77 | ### Metadata 78 | 79 | Along with the data, the hook will also return the `ref`, `source`, and `fetchTime` metadata from the mutation. 80 | 81 | ```tsx 82 | const createMovie = useDataConnectMutation(createMovieRef); 83 | 84 | const { dataConnectResult } = await createMovie.mutateAsync({ 85 | title: 'John Wick', 86 | genre: "Action", 87 | imageUrl: "https://example.com/image.jpg", 88 | }); 89 | 90 | console.log(dataConnectResult.ref); 91 | console.log(dataConnectResult.source); 92 | console.log(dataConnectResult.fetchTime); 93 | ``` -------------------------------------------------------------------------------- /docs/react/data-connect/querying.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Querying 3 | description: Learn how to query data from Firebase Data Connect using the Tanstack Query Firebase hooks. 4 | --- 5 | 6 | ## Querying Data 7 | 8 | To query data from Firebase Data Connect, you can use the `useDataConnectQuery` hook. This hook will automatically infer the data type from the connector and the query and automatically create a [query key](https://tanstack.com/query/latest/docs/framework/react/guides/query-keys) for the query. 9 | 10 | ```tsx 11 | import { useDataConnectQuery } from "@tanstack-query-firebase/react/data-connect"; 12 | import { listMoviesRef } from "@dataconnect/default-connector"; 13 | 14 | function Component() { 15 | const { data, isPending, isSuccess, isError, error } = useDataConnectQuery( 16 | listMoviesRef() 17 | ); 18 | } 19 | ``` 20 | 21 | ### Query options 22 | 23 | To leverage the full power of Tanstack Query, you can pass in query options to the `useDataConnectQuery` hook, for example to refetch the query on a interval: 24 | 25 | ```tsx 26 | const { data, isPending, isSuccess, isError, error } = useDataConnectQuery( 27 | listMoviesRef(), 28 | { 29 | refetchInterval: 1000, 30 | } 31 | ); 32 | ``` 33 | 34 | The hook extends the [`useQuery`](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery) hook, so you can learn more about the available options by reading the [Tanstack Query documentation](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery). 35 | 36 | ### Overriding the query key 37 | 38 | To override the query key, you can pass in a custom query key to the `useDataConnectQuery` hook: 39 | 40 | ```tsx 41 | const { data, isPending, isSuccess, isError, error } = useDataConnectQuery( 42 | getMovieRef({ id: "1" }), 43 | { 44 | queryKey: ["movies", "1"], 45 | } 46 | ); 47 | ``` 48 | 49 | Note that overriding the query key could mean your query is no longer synchronized with mutation invalidations or server side rendering pre-fetching. 50 | 51 | ### Initial data 52 | 53 | If your application has already fetched a data from Data Connect, you can instead pass the `QueryResult` instance to the hook. This will instead set the `initialData` option on the hook: 54 | 55 | ```tsx 56 | // Elsewhere in your application 57 | const movies = await executeQuery(listMoviesRef()); 58 | 59 | // ... 60 | 61 | function Component(props: { movies: QueryResult }) { 62 | const { data, isPending, isSuccess, isError, error } = useDataConnectQuery( 63 | props.movies 64 | ); 65 | } 66 | ``` 67 | 68 | The hook will immediately have data available, and immediately refetch the data when the component is mounted. This behavior can be contolled by providing a `staleTime` value to the hook or Query Client. 69 | 70 | ### Metadata 71 | 72 | Along with the data, the hook will also return the `ref`, `source`, and `fetchTime` metadata from the query. 73 | 74 | ```tsx 75 | const { dataConnectResult } = useDataConnectQuery(listMoviesRef()); 76 | 77 | console.log(dataConnectResult?.ref); 78 | console.log(dataConnectResult?.source); 79 | console.log(dataConnectResult?.fetchTime); 80 | ``` 81 | -------------------------------------------------------------------------------- /docs/react/data-connect/server-side-rendering.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Server Side Rendering 3 | description: Learn how to use Tanstack Query Firebase hooks for server side rendering. 4 | --- 5 | 6 | ## Server Side Rendering 7 | 8 | The Data Connect package provides a `DataConnectQueryClient` class that extends the `QueryClient` class. This class provides an additional method called `prefetchDataConnectQuery` that allows you to prefetch data for server side rendering. 9 | 10 | ## Server Side Rendering (with Next.js) 11 | 12 | Using [traditional server side rendering](https://tanstack.com/query/latest/docs/framework/react/guides/ssr), the query client instance can be passed to the client "dehydrated", and then rehydrated on the client side. 13 | 14 | The following example demonstrates how to do this with the `DataConnectQueryClient` class and a Next.js application. The data will be immediately available to the client, and the query client will be hydrated on the client side. 15 | 16 | > Ensure you have followed the [initial setup](https://tanstack.com/query/latest/docs/framework/react/guides/ssr#initial-setup) steps first! 17 | 18 | ```tsx 19 | import type { InferGetStaticPropsType } from "next"; 20 | import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; 21 | import { listMoviesRef } from "@dataconnect/default-connector"; 22 | import { DataConnectQueryClient } from "@tanstack-query-firebase/react/data-connect"; 23 | 24 | export async function getStaticProps() { 25 | const queryClient = new DataConnectQueryClient(); 26 | 27 | // Prefetch the list of movies 28 | await queryClient.prefetchDataConnectQuery(listMoviesRef()); 29 | 30 | return { 31 | props: { 32 | dehydratedState: dehydrate(queryClient), 33 | }, 34 | }; 35 | } 36 | 37 | export default function MoviesRoute({ 38 | dehydratedState, 39 | }: InferGetStaticPropsType) { 40 | return ( 41 | 42 | 43 | 44 | ); 45 | } 46 | 47 | function Movies() { 48 | const movies = useDataConnectQuery(listMoviesRef()); 49 | 50 | if (movies.isLoading) { 51 | return
Loading...
; 52 | } 53 | 54 | if (movies.isError) { 55 | return
Error: {movies.error.message}
; 56 | } 57 | 58 | return ( 59 |
60 |

Movies

61 |
    62 | {movies.data!.movies.map((movie) => ( 63 |
  • {movie.title}
  • 64 | ))} 65 |
66 |
67 | ); 68 | } 69 | ``` 70 | 71 | ### React Server Components (RSC) 72 | 73 | If you are oping into using React Server Components, you can similarly use the `DataConnectQueryClient` class to prefetch data within your server component: 74 | 75 | ```tsx 76 | import { Movies } from "@/examples/data-connect"; 77 | import { listMoviesRef } from "@dataconnect/default-connector"; 78 | import { DataConnectQueryClient } from "@tanstack-query-firebase/react/data-connect"; 79 | import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; 80 | 81 | import "@/firebase"; 82 | 83 | export default async function PostsPage() { 84 | const queryClient = new DataConnectQueryClient(); 85 | 86 | // Prefetch the list of movies 87 | await queryClient.prefetchDataConnectQuery(listMoviesRef()); 88 | 89 | return ( 90 | 91 | 92 | 93 | ); 94 | } 95 | 96 | function Movies() { 97 | const movies = useDataConnectQuery(listMoviesRef()); 98 | 99 | // ... 100 | } 101 | ``` 102 | 103 | ### Gotchas 104 | 105 | - If you opt-in to providing a custom `queryKey` to either the prefetched data or the `useDataConnectQuery` hook, you must ensure that the `queryKey` is the same for both. 106 | - By default, the client will always refetch data in the background. If this behaviour is not desired, you can set the `staleTime` option in your Query Client or hook options. 107 | -------------------------------------------------------------------------------- /docs/react/firestore/hooks/useClearIndexedDbPersistenceMutation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useClearIndexedDbPersistenceMutation 3 | --- 4 | 5 | A mutation which wraps the [`clearIndexedDbPersistence`](https://firebase.google.com/docs/reference/js/firestore_.md#clearindexeddbpersistence_231a8e0) function. 6 | 7 | ## Usage 8 | 9 | ```jsx 10 | import { getFirestore } from 'firebase/firestore'; 11 | import { clearIndexedDbPersistence } from '@tanstack-query-firebase/react/firestore'; 12 | 13 | // Get a Firestore instance using the initialized Firebase app instance 14 | const firestore = getFirestore(app); 15 | 16 | function Component() { 17 | const mutation = useClearIndexedDbPersistenceMutation(firestore); 18 | 19 | return ( 20 | 23 | ); 24 | } 25 | ``` 26 | 27 | ## Mutation Options 28 | 29 | The hook also accepts the [`useMutation`](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation) 30 | options, for example: 31 | 32 | ```tsx 33 | const mutation = useClearIndexedDbPersistenceMutation(firestore, { 34 | onSuccess() { 35 | console.log('IndexedDB persistence cleared'); 36 | }, 37 | onError(error) { 38 | console.error('Failed to clear IndexedDB persistence', error); 39 | }, 40 | }); 41 | ``` -------------------------------------------------------------------------------- /docs/react/firestore/hooks/useCollectionQuery.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useCollectionQuery 3 | --- 4 | 5 | This hook is implemented, but the documentation is a work in progress, check back soon. -------------------------------------------------------------------------------- /docs/react/firestore/hooks/useDisableNetworkMutation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useDisableNetworkMutation 3 | --- 4 | 5 | This hook is implemented, but the documentation is a work in progress, check back soon. -------------------------------------------------------------------------------- /docs/react/firestore/hooks/useDocumentQuery.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useDocumentQuery 3 | --- 4 | 5 | This hook is implemented, but the documentation is a work in progress, check back soon. -------------------------------------------------------------------------------- /docs/react/firestore/hooks/useEnableNetworkMutation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useEnableNetworkMutation 3 | --- 4 | 5 | This hook is implemented, but the documentation is a work in progress, check back soon. -------------------------------------------------------------------------------- /docs/react/firestore/hooks/useGetAggregateFromServerQuery.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useGetAggregateFromServerQuery 3 | --- 4 | 5 | This hook is implemented, but the documentation is a work in progress, check back soon. -------------------------------------------------------------------------------- /docs/react/firestore/hooks/useGetCountFromServerQuery.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useGetCountFromServerQuery 3 | --- 4 | 5 | This hook is implemented, but the documentation is a work in progress, check back soon. -------------------------------------------------------------------------------- /docs/react/firestore/hooks/useRunTransactionMutation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useRunTransactionMutation 3 | --- 4 | 5 | This hook is implemented, but the documentation is a work in progress, check back soon. -------------------------------------------------------------------------------- /docs/react/firestore/hooks/useWaitForPendingWritesQuery.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useWaitForPendingWritesQuery 3 | --- 4 | 5 | This hook is implemented, but the documentation is a work in progress, check back soon. -------------------------------------------------------------------------------- /docs/react/firestore/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Firebase Firestore 3 | --- 4 | 5 | ## Setup 6 | 7 | Before using the Tanstack Query Firebase hooks for Firestore, ensure you have configured your Firebase application 8 | to setup an Firestore instance: 9 | 10 | ```ts 11 | import { initializeApp } from "firebase/app"; 12 | import { getFirestore } from "firebase/firestore"; 13 | 14 | // Initialize your Firebase app 15 | initializeApp({ ... }); 16 | 17 | // Get the Firestore instance 18 | const firestore = getFirestore(app); 19 | ``` 20 | 21 | ## Importing 22 | 23 | The package exports are available via the `@tanstack-query-firebase/react` package under the `firestore` namespace: 24 | 25 | ```ts 26 | import { useDocumentQuery } from "@tanstack-query-firebase/react/firestore"; 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/react/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: React 3 | description: Using TanStack Query Firebase with React 4 | --- 5 | 6 | To get started using TanStack Query Firebase with React, you will need to install the following packages: 7 | 8 | ```bash 9 | npm i --save firebase @tanstack/react-query @tanstack-query-firebase/react 10 | ``` 11 | 12 | Both `firebase` and `@tanstack/react-query` are peer dependencies of `@tanstack-query-firebase/react`. 13 | 14 | ## Usage 15 | 16 | TanStack Query Firebase provides a hands-off approach to integrate with TanStack Query - you are 17 | still responsible for both setting up Firebase in your application and configuring TanStack Query. 18 | 19 | If you haven't already done so, [initialize your Firebase project](https://firebase.google.com/docs/web/setup) 20 | and [configure TanStack Query](https://tanstack.com/query/latest/docs/framework/react/quick-start): 21 | 22 | ```jsx 23 | import { initializeApp } from 'firebase/app'; 24 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 25 | 26 | // TODO: Replace the following with your app's Firebase project configuration 27 | const firebaseConfig = { 28 | //... 29 | }; 30 | 31 | // Initialize Firebase 32 | const app = initializeApp(firebaseConfig); 33 | 34 | // Create a TanStack Query client instance 35 | const queryClient = new QueryClient() 36 | 37 | function App() { 38 | return ( 39 | // Provide the client to your App 40 | 41 | 42 | 43 | ) 44 | } 45 | 46 | render(, document.getElementById('root')) 47 | ``` 48 | 49 | Next, you can start to use the hooks provided by `@tanstack-query-firebase/react`. For example, to 50 | fetch a document from Firestore: 51 | 52 | ```jsx 53 | import { getFirestore, doc } from 'firebase/firestore'; 54 | import { useDocumentQuery } from '@tanstack-query-firebase/react/firestore'; 55 | 56 | // Get a Firestore instance using the initialized Firebase app instance 57 | const firestore = getFirestore(app); 58 | 59 | function MyApplication() { 60 | // Create a document reference using Firestore 61 | const docRef = doc(firestore, 'cities', 'SF'); 62 | 63 | // Fetch the document using the useDocumentQuery hook 64 | const query = useDocumentQuery(docRef); 65 | 66 | if (query.isLoading) { 67 | return

Loading data...

; 68 | } 69 | 70 | if (query.isError) { 71 | return

Error fetching data: {query.error.code}

; 72 | } 73 | 74 | // The successful result of the query is a DocumentSnapshot from Firebase 75 | const snapshot = query.data; 76 | 77 | if (!snapshot.exists()) { 78 | return

Document does not exist

; 79 | } 80 | 81 | const data = snapshot.data(); 82 | 83 | return ( 84 |
85 |

{data.name}

86 |

{data.city}

87 |
88 | ); 89 | } 90 | ``` 91 | 92 | TanStack Query Firebase provides hooks for all Firebase services, supporting both mutations and queries. 93 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from "@eslint/eslintrc"; 2 | import { dirname } from "path"; 3 | import { fileURLToPath } from "url"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-react-data-connect", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "npx next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@tanstack/react-query": "^5.55.4", 13 | "@tanstack-query-firebase/react": "workspace:*", 14 | "@dataconnect/default-connector": "file:../../../dataconnect-sdk/js/default-connector", 15 | "firebase": "^11.3.0", 16 | "next": "15.1.0", 17 | "react": "^19.2.1", 18 | "react-dom": "^19.2.1" 19 | }, 20 | "devDependencies": { 21 | "@eslint/eslintrc": "^3", 22 | "@types/node": "^20", 23 | "@types/react": "^19", 24 | "@types/react-dom": "^19", 25 | "eslint": "^9", 26 | "eslint-config-next": "15.1.0", 27 | "postcss": "^8", 28 | "tailwindcss": "^3.4.1", 29 | "typescript": "^5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Providers from "./providers"; 2 | 3 | export default function RootLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return ( 9 | 10 | 11 | 12 | {children} 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | // Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top 4 | import { 5 | isServer, 6 | QueryClient, 7 | QueryClientProvider, 8 | } from "@tanstack/react-query"; 9 | 10 | function makeQueryClient() { 11 | return new QueryClient({ 12 | defaultOptions: { 13 | queries: { 14 | // With SSR, we usually want to set some default staleTime 15 | // above 0 to avoid refetching immediately on the client 16 | staleTime: 60 * 1000, 17 | }, 18 | }, 19 | }); 20 | } 21 | 22 | let browserQueryClient: QueryClient | undefined; 23 | 24 | function getQueryClient() { 25 | if (isServer) { 26 | // Server: always make a new query client 27 | return makeQueryClient(); 28 | } 29 | // Browser: make a new query client if we don't already have one 30 | // This is very important, so we don't re-make a new client if React 31 | // suspends during the initial render. This may not be needed if we 32 | // have a suspense boundary BELOW the creation of the query client 33 | if (!browserQueryClient) browserQueryClient = makeQueryClient(); 34 | return browserQueryClient; 35 | } 36 | 37 | export default function Providers({ children }: { children: React.ReactNode }) { 38 | // NOTE: Avoid useState when initializing the query client if you don't 39 | // have a suspense boundary between this and the code that may 40 | // suspend because React will throw away the client on the initial 41 | // render if it suspends and there is no boundary 42 | const queryClient = getQueryClient(); 43 | 44 | return ( 45 | {children} 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/src/app/rsc/data-connect/page.tsx: -------------------------------------------------------------------------------- 1 | import { listMoviesRef } from "@dataconnect/default-connector"; 2 | import { 3 | dehydrate, 4 | HydrationBoundary, 5 | QueryClient, 6 | } from "@tanstack/react-query"; 7 | import { executeQuery } from "firebase/data-connect"; 8 | import { Movies } from "@/examples/data-connect"; 9 | 10 | import "@/firebase"; 11 | 12 | // Force dynamic rendering to avoid build-time Firebase calls 13 | export const dynamic = "force-dynamic"; 14 | 15 | export default async function PostsPage() { 16 | const queryClient = new QueryClient(); 17 | const result = await executeQuery(listMoviesRef()); 18 | 19 | queryClient.setQueryData( 20 | [result.ref.name, result.ref.variables], 21 | result.data, 22 | ); 23 | 24 | return ( 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/src/examples/data-connect.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createMovieRef, listMoviesRef } from "@dataconnect/default-connector"; 4 | import { 5 | useDataConnectMutation, 6 | useDataConnectQuery, 7 | } from "@tanstack-query-firebase/react/data-connect"; 8 | 9 | import "@/firebase"; 10 | 11 | export function Movies() { 12 | const movies = useDataConnectQuery(listMoviesRef()); 13 | 14 | const addMovie = useDataConnectMutation(createMovieRef, { 15 | invalidate: [listMoviesRef()], 16 | }); 17 | 18 | if (movies.isLoading) { 19 | return
Loading...
; 20 | } 21 | 22 | if (movies.isError) { 23 | return
Error: {movies.error.message}
; 24 | } 25 | 26 | return ( 27 |
28 |

Movies

29 | 41 |
    42 |
  • Fetch Time: {movies.dataConnectResult?.fetchTime}
  • 43 |
  • Source: {movies.dataConnectResult?.source}
  • 44 |
  • 45 | Query Key: {movies.dataConnectResult?.ref.name} +{" "} 46 | {movies.dataConnectResult?.ref.variables} 47 |
  • 48 | {movies.data!.movies.map((movie) => ( 49 |
  • {movie.title}
  • 50 | ))} 51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/src/firebase.ts: -------------------------------------------------------------------------------- 1 | import { connectorConfig } from "@dataconnect/default-connector"; 2 | import { getApps, initializeApp } from "firebase/app"; 3 | import { 4 | connectDataConnectEmulator, 5 | getDataConnect, 6 | } from "firebase/data-connect"; 7 | 8 | if (getApps().length === 0) { 9 | initializeApp({ 10 | projectId: "example", 11 | }); 12 | const dataConnect = getDataConnect(connectorConfig); 13 | connectDataConnectEmulator(dataConnect, "localhost", 9399); 14 | } 15 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | import type { AppProps } from "next/app"; 5 | import React from "react"; 6 | 7 | export default function MyApp({ Component, pageProps }: AppProps) { 8 | const [queryClient] = React.useState( 9 | () => 10 | new QueryClient({ 11 | defaultOptions: { 12 | queries: { 13 | // With SSR, we usually want to set some default staleTime 14 | // above 0 to avoid refetching immediately on the client 15 | staleTime: 60 * 1000, 16 | }, 17 | }, 18 | }), 19 | ); 20 | 21 | return ( 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/src/pages/client/data-connect.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Movies } from "@/examples/data-connect"; 4 | 5 | export default function MoviesRoute() { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/src/pages/ssr/data-connect.tsx: -------------------------------------------------------------------------------- 1 | import { listMoviesRef } from "@dataconnect/default-connector"; 2 | import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; 3 | import { DataConnectQueryClient } from "@tanstack-query-firebase/react/data-connect"; 4 | import type { InferGetStaticPropsType } from "next"; 5 | import { Movies } from "@/examples/data-connect"; 6 | 7 | export async function getStaticProps() { 8 | const queryClient = new DataConnectQueryClient(); 9 | 10 | await queryClient.prefetchDataConnectQuery(listMoviesRef()); 11 | 12 | return { 13 | props: { 14 | dehydratedState: dehydrate(queryClient), 15 | }, 16 | }; 17 | } 18 | 19 | export default function MoviesRoute({ 20 | dehydratedState, 21 | }: InferGetStaticPropsType) { 22 | return ( 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: "var(--background)", 13 | foreground: "var(--foreground)", 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } satisfies Config; 19 | -------------------------------------------------------------------------------- /examples/react/react-data-connect/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/react/useGetIdTokenQuery/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/react/useGetIdTokenQuery/README.md: -------------------------------------------------------------------------------- 1 | # Firebase Authentication Example (Vite) 2 | 3 | Simple Vite React app demonstrating Firebase Authentication with TanStack Query. 4 | 5 | ## Quick Start 6 | 7 | ```bash 8 | # Install dependencies 9 | pnpm install 10 | 11 | # Run with emulators (recommended) 12 | pnpm dev:emulator 13 | 14 | # Or run without emulators 15 | pnpm dev 16 | ``` 17 | 18 | ## Features 19 | 20 | - **ID Token Management** - `useGetIdTokenQuery` hook demo 21 | 22 | -------------------------------------------------------------------------------- /examples/react/useGetIdTokenQuery/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/react/useGetIdTokenQuery/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-useGetIdTokenQuery", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "dev:emulator": "cd ../../../ && firebase emulators:exec --project test-project 'cd examples/react/useGetIdTokenQuery && vite'", 9 | "build": "npx vite build", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@tanstack-query-firebase/react": "workspace:*", 14 | "@tanstack/react-query": "^5.66.9", 15 | "@tanstack/react-query-devtools": "^5.84.2", 16 | "firebase": "^11.3.1", 17 | "react": "^19.2.1", 18 | "react-dom": "^19.2.1" 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^19.1.9", 22 | "@types/react-dom": "^19.1.7", 23 | "@vitejs/plugin-react": "^4.7.0", 24 | "autoprefixer": "^10.4.21", 25 | "postcss": "^8.5.6", 26 | "tailwindcss": "^3.4.17", 27 | "typescript": "~5.8.3", 28 | "vite": "^7.1.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/react/useGetIdTokenQuery/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/react/useGetIdTokenQuery/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /examples/react/useGetIdTokenQuery/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react/useGetIdTokenQuery/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 3 | import { useState } from "react"; 4 | import { IdTokenExample } from "./components/IdTokenExample"; 5 | 6 | import "./firebase"; 7 | 8 | function App() { 9 | const [queryClient] = useState( 10 | () => 11 | new QueryClient({ 12 | defaultOptions: { 13 | queries: { 14 | staleTime: 60 * 1000, 15 | }, 16 | }, 17 | }), 18 | ); 19 | 20 | return ( 21 | 22 |
23 |
24 |
25 |

26 | Firebase Authentication Examples 27 |

28 |

29 | TanStack Query Firebase Authentication hooks and patterns 30 |

31 |
32 | 33 |
34 | 35 |
36 | 37 |
38 |

39 | Built with Vite, TanStack Query, and Firebase Auth 40 |

41 |
42 |
43 |
44 | 45 |
46 | ); 47 | } 48 | 49 | export default App; 50 | -------------------------------------------------------------------------------- /examples/react/useGetIdTokenQuery/src/firebase.ts: -------------------------------------------------------------------------------- 1 | import { getApps, initializeApp } from "firebase/app"; 2 | import { connectAuthEmulator, getAuth } from "firebase/auth"; 3 | 4 | if (getApps().length === 0) { 5 | initializeApp({ 6 | projectId: "example", 7 | apiKey: "demo-api-key", // Required for Firebase to initialize 8 | }); 9 | 10 | // Connect to Auth emulator if running locally 11 | if (import.meta.env.DEV) { 12 | try { 13 | const auth = getAuth(); 14 | connectAuthEmulator(auth, "http://localhost:9099"); 15 | console.log("Connected to Firebase Auth emulator"); 16 | } catch (error) { 17 | console.warn("Could not connect to Firebase Auth emulator:", error); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/react/useGetIdTokenQuery/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/react/useGetIdTokenQuery/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App.tsx"; 5 | 6 | createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /examples/react/useGetIdTokenQuery/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react/useGetIdTokenQuery/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /examples/react/useGetIdTokenQuery/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /examples/react/useGetIdTokenQuery/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src", "vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/react/useGetIdTokenQuery/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import { defineConfig } from "vite"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | build: { 8 | rollupOptions: { 9 | external: ["@tanstack-query-firebase/react/auth"], 10 | }, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "emulators": { 3 | "firestore": { 4 | "port": 8080 5 | }, 6 | "auth": { 7 | "port": 9099 8 | }, 9 | "functions": { 10 | "port": 5001 11 | }, 12 | "database": { 13 | "port": 9000 14 | }, 15 | "dataconnect": { 16 | "port": 9399 17 | }, 18 | "ui": { 19 | "enabled": true 20 | } 21 | }, 22 | "functions": { 23 | "predeploy": "npm --prefix \"$RESOURCE_DIR\" run build", 24 | "source": "functions" 25 | }, 26 | "firestore": { 27 | "rules": "./firestore.rules" 28 | }, 29 | "dataconnect": { 30 | "source": "dataconnect" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | match /tests/{document=**} { 5 | allow read: if true; 6 | allow write: if true; 7 | allow create: if true; 8 | allow get: if true; 9 | } 10 | // match /noread/{document=**} { 11 | // allow read: if false; 12 | // } 13 | } 14 | } -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "build": "", 5 | "serve": "firebase emulators:start --only functions --project react-query-firebase", 6 | "shell": "firebase functions:shell", 7 | "start": "npm run shell", 8 | "deploy": "firebase deploy --only functions --project react-query-firebase", 9 | "logs": "firebase functions:log" 10 | }, 11 | "engines": { 12 | "node": "20" 13 | }, 14 | "main": "src/index.js", 15 | "dependencies": { 16 | "axios": "^0.21.4", 17 | "firebase-admin": "^9.8.0", 18 | "firebase-functions": "^3.14.1" 19 | }, 20 | "private": true 21 | } 22 | -------------------------------------------------------------------------------- /functions/src/index.js: -------------------------------------------------------------------------------- 1 | const functions = require("firebase-functions"); 2 | const axios = require("axios"); 3 | 4 | module.exports.test = functions.https.onCall((data) => { 5 | return { 6 | response: data, 7 | }; 8 | }); 9 | 10 | module.exports.getJoke = functions.https.onCall(async (_data) => { 11 | return axios("https://api.icndb.com/jokes/random").then( 12 | (res) => res.data.value.joke, 13 | ); 14 | }); 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tanstack-query-firebase/root", 3 | "private": true, 4 | "packageManager": "pnpm@10.10.0", 5 | "scripts": { 6 | "test": "turbo test", 7 | "test:emulator": "firebase emulators:exec --project test-project \"pnpm turbo test:ci\"", 8 | "serve:coverage": "npx serve coverage", 9 | "emulator": "firebase emulators:start --project test-project", 10 | "emulator:kill": "lsof -t -i:4001 -i:8080 -i:9000 -i:9099 -i:9199 -i:8085 -i:9399 -i:9299 | xargs kill -9", 11 | "format": "biome check .", 12 | "format:fix": "biome check . --write", 13 | "changeset": "changeset", 14 | "version": "changeset version", 15 | "release": "pnpm build && changeset publish", 16 | "build": "turbo build" 17 | }, 18 | "devDependencies": { 19 | "@angular/core": "^20.0.0", 20 | "@biomejs/biome": "2.1.1", 21 | "@changesets/cli": "^2.29.4", 22 | "@tanstack/angular-query-experimental": "^5.66.4", 23 | "@tanstack/react-query": "^5.55.4", 24 | "@types/jsonwebtoken": "^9.0.7", 25 | "@vitest/coverage-istanbul": "^2.0.5", 26 | "firebase": "^11.3.0", 27 | "happy-dom": "^15.7.3", 28 | "jsonwebtoken": "^9.0.2", 29 | "react": "^19.2.1", 30 | "tsup": "^8.2.4", 31 | "turbo": "^2.5.3", 32 | "typescript": "^5.6.2", 33 | "vitest": "^2.0.5" 34 | }, 35 | "dependencies": { 36 | "@angular/fire": "^20.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/angular/.gitignore: -------------------------------------------------------------------------------- 1 | data-connect/* 2 | index.js -------------------------------------------------------------------------------- /packages/angular/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @tanstack-query-firebase/angular 2 | 3 | ## 1.0.3 4 | 5 | ### Patch Changes 6 | 7 | - ee67d1e: Fix issue where signal updates weren't getting picked up 8 | 9 | ## 1.0.2 10 | 11 | ### Patch Changes 12 | 13 | - c966946: chore: add repository fields to package.json 14 | 15 | ## 1.0.1 16 | 17 | ### Patch Changes 18 | 19 | - b807700: Improve various CI processes and add changeset support 20 | -------------------------------------------------------------------------------- /packages/angular/README.md: -------------------------------------------------------------------------------- 1 | # @tanstack-query-firebase/angular 2 | 3 | `@tanstack-query-firebase/angular` provides angular bindings for Firebase products. 4 | 5 | ## Install Dependencies 6 | 7 | ```bash 8 | npm i --save @angular/fire firebase @tanstack-query-firebase/angular 9 | ``` 10 | 11 | ## Usage 12 | 13 | ### Data Connect 14 | 15 | In your `app.config.ts`, add `provideTanstack` as a provider and `provideDataConnect` and `provideFirebaseApp`: 16 | 17 | ```ts 18 | import { initializeApp } from '@angular/fire/app'; 19 | import { provideDataConnect, getDataConnect } from '@angular/fire/data-connect'; 20 | import { connectorConfig } from '@myorg/movies'; // Replace with your generated package name 21 | 22 | export const appConfig: ApplicationConfig = { 23 | providers: [ 24 | ... 25 | provideTanStackQuery(new QueryClient()), 26 | provideFirebaseApp(() => initializeFirebase({/*paste your config here*/})), 27 | provideDataConnect(() => { 28 | const dc = getDataConnect(connectorConfig); 29 | // Add below to connect to the Data Connect emulator 30 | // connectDataConnectEmulator(dc, 'localhost', 9399); 31 | return dc; 32 | }), 33 | ], 34 | }; 35 | ``` 36 | 37 | #### Calling Queries 38 | 39 | ```ts 40 | import { injectDataConnectQuery } from '@tanstack-query-firebase/angular'; 41 | import { listMoviesRef } from '@myorg/movies/angular'; 42 | 43 | @Component({ 44 | ..., 45 | template: ` 46 | 47 | @if (query.isPending()) { Loading... } @if (query.error()) { An error 48 | has occurred: {{ query.error()?.message }} 49 | } @if (query.data(); as data) { @for (movie of data.movies ; track 50 | movie.id) { 51 | 52 | 53 | {{ movie.name }} 54 | 55 | {{ movie.synopsis }} 56 | 57 | 58 | 59 | } @empty { Empty list of movies } } 60 | 61 | ` 62 | }) 63 | export class MovieListComponent { 64 | public query = injectDataConnectQuery(listMoviesRef()) 65 | } 66 | ``` 67 | 68 | #### Adding options 69 | 70 | ```ts 71 | ... 72 | public query = injectDataConnectQuery(listMoviesRef(), () => ({ 73 | enabled: false 74 | })); 75 | ``` 76 | 77 | #### Calling Mutations 78 | 79 | ```ts 80 | import { injectDataConnectMutation } from '@tanstack-query-firebase/angular'; 81 | import { addMovieRef } from '@myorg/movies/angular'; 82 | @Component({ 83 | ..., 84 | template: ` 85 | ... 86 | 93 | ` 94 | }) 95 | export class MovieListComponent { 96 | public mutation = injectDataConnectQuery(addMovieRef()) 97 | addGeneratedMovie() { 98 | mutation.mutate({ // Or you can use `mutateAsync` 99 | name: 'Random Movie ' + this.query.data()?.length, 100 | genre: 'Some Genre', 101 | synopsis: 'Random Synopsis', 102 | }); 103 | } 104 | } 105 | ``` 106 | 107 | ##### Adding options 108 | 109 | We allow invalidating other related queries by: 110 | 111 | ```ts 112 | ... 113 | public mutation = injectDataConnectQuery(addMovieRef(), () => ({ 114 | invalidate: [listMoviesRef()] 115 | })) 116 | ``` 117 | 118 | You can also pass in other valid options from [CreateMutationOptions](https://tanstack.com/query/latest/docs/framework/angular/reference/interfaces/createmutationoptions). -------------------------------------------------------------------------------- /packages/angular/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tanstack-query-firebase/angular", 3 | "version": "1.0.3", 4 | "description": "TanStack Query bindings for Firebase and Angular", 5 | "type": "module", 6 | "scripts": { 7 | "build": "tsup", 8 | "watch": "tsup --watch", 9 | "test": "vitest --dom --coverage", 10 | "test:ci": "vitest --dom --coverage --run", 11 | "publish-package": "pnpm run build && cd dist && npm publish" 12 | }, 13 | "typings": "./index.d.ts", 14 | "module": "./index.js", 15 | "exports": { 16 | ".": { 17 | "types": "./index.d.ts", 18 | "import": "./index.js", 19 | "default": "./data-connect/index.js" 20 | }, 21 | "./data-connect": { 22 | "import": "./data-connect/index.js", 23 | "types": "./data-connect/index.d.ts", 24 | "default": "./data-connect/index.js" 25 | } 26 | }, 27 | "author": { 28 | "name": "Invertase", 29 | "email": "oss@invertase.io", 30 | "url": "https://github.com/invertase/tanstack-query-firebase" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/invertase/tanstack-query-firebase" 35 | }, 36 | "license": "Apache-2.0", 37 | "devDependencies": { 38 | "@dataconnect/default-connector": "file:../../dataconnect-sdk/js/default-connector", 39 | "@testing-library/angular": "^18.0.0", 40 | "@testing-library/dom": "^10.4.0", 41 | "tsup": "^8.4.0" 42 | }, 43 | "peerDependencies": { 44 | "@angular/common": "^20.0.0 || ^19.0.0", 45 | "@angular/core": "^20.0.0 || ^19.0.0", 46 | "@angular/fire": "^20.0.0 || ^19.0.0", 47 | "@angular/platform-browser-dynamic": "^20.0.0 || ^19.0.0", 48 | "@tanstack/angular-query-experimental": "^5.66.4" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/angular/src/data-connect/types.ts: -------------------------------------------------------------------------------- 1 | import type { Signal } from "@angular/core"; 2 | import type { 3 | CreateMutationResult, 4 | CreateQueryResult, 5 | } from "@tanstack/angular-query-experimental"; 6 | import type { FirebaseError } from "firebase/app"; 7 | import type { MutationResult, QueryResult } from "firebase/data-connect"; 8 | 9 | export type CreateDataConnectQueryResult = CreateQueryResult< 10 | Data, 11 | FirebaseError 12 | > & { 13 | dataConnectResult: Signal> | undefined>; 14 | }; 15 | 16 | export type CreateDataConnectMutationResult = 17 | CreateMutationResult & { 18 | dataConnectResult: Signal< 19 | Partial> | undefined 20 | >; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/angular/src/index.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /packages/angular/test-setup.ts: -------------------------------------------------------------------------------- 1 | import { getTestBed } from "@angular/core/testing"; 2 | import { 3 | BrowserDynamicTestingModule, 4 | platformBrowserDynamicTesting, 5 | } from "@angular/platform-browser-dynamic/testing"; 6 | 7 | getTestBed().initTestEnvironment( 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting(), 10 | ); 11 | -------------------------------------------------------------------------------- /packages/angular/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "jsx": "react", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "resolveJsonModule": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "types": ["vitest/globals"], 14 | "paths": { 15 | "~/testing-utils": ["../react/vitest/utils.ts"], 16 | "@/dataconnect/*": ["../../dataconnect-sdk/js/*"] 17 | } 18 | }, 19 | "include": ["src", "utils.tsx", "./package.json"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/angular/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import { defineConfig } from "tsup"; 3 | 4 | export default defineConfig({ 5 | entry: ["src/data-connect/index.ts", "src/index.ts"], 6 | format: ["esm"], 7 | dts: true, // generates .d.ts files 8 | outDir: "./dist", 9 | clean: true, 10 | esbuildOptions(options) { 11 | options.outbase = "./src"; 12 | }, 13 | async onSuccess() { 14 | try { 15 | await fs.copyFile("./package.json", "./dist/package.json"); 16 | await fs.copyFile("./README.md", "./dist/README.md"); 17 | await fs.copyFile("./LICENSE", "./dist/LICENSE"); 18 | } catch (e) { 19 | console.error(`Error copying files: ${e}`); 20 | } 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /packages/angular/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { defineConfig } from "vitest/config"; 3 | 4 | import packageJson from "./package.json"; 5 | 6 | export default defineConfig({ 7 | resolve: { 8 | alias: { 9 | "~/testing-utils": path.resolve(__dirname, "../../vitest/utils"), 10 | "@/dataconnect/default-connector": path.resolve( 11 | __dirname, 12 | "../../dataconnect-sdk/js/default-connector", 13 | ), 14 | }, 15 | }, 16 | test: { 17 | fakeTimers: { 18 | toFake: ["setTimeout", "clearTimeout", "Date"], 19 | }, 20 | name: packageJson.name, 21 | dir: "./src", 22 | watch: false, 23 | environment: "happy-dom", 24 | setupFiles: ["test-setup.ts"], 25 | coverage: { enabled: true, provider: "istanbul", include: ["src/**/*"] }, 26 | typecheck: { enabled: true }, 27 | globals: true, 28 | restoreMocks: true, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /packages/react/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @tanstack-query-firebase/react 2 | 3 | ## 2.1.2 4 | 5 | ### Patch Changes 6 | 7 | - fab612f: Updated dependencies to depend on react version 19.2.1 8 | 9 | ## 2.1.1 10 | 11 | ### Patch Changes 12 | 13 | - c966946: chore: add repository fields to package.json 14 | 15 | ## 2.1.0 16 | 17 | ### Minor Changes 18 | 19 | - 802e2f1: Add support for Firebase v12 20 | 21 | - Updated Firebase peer dependency from v11.3.0 to v12.0.0 22 | - Updated TypeScript target from ES2015 to ES2020 to align with Firebase v12 requirements 23 | - All existing functionality remains compatible with Firebase v12 24 | 25 | - 2589791: add useGetIdTokenQuery and update examples directory 26 | 27 | ### Patch Changes 28 | 29 | - b807700: Improve various CI processes and add changeset support 30 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tanstack-query-firebase/react", 3 | "version": "2.1.2", 4 | "description": "TanStack Query bindings for Firebase and React", 5 | "type": "module", 6 | "scripts": { 7 | "test": "vitest --dom --coverage", 8 | "test:ci": "vitest --dom --coverage --run", 9 | "build": "tsup", 10 | "serve:coverage": "npx serve coverage", 11 | "emulator": "firebase emulators:start --project test-project", 12 | "emulator:kill": "lsof -t -i:4001 -i:8080 -i:9000 -i:9099 -i:9199 -i:8085 | xargs kill -9", 13 | "check": "tsc --noEmit", 14 | "publish-package": "pnpm run build && cd dist && npm publish" 15 | }, 16 | "exports": { 17 | ".": { 18 | "import": "./dist/index.js", 19 | "types": "./dist/index.d.ts" 20 | }, 21 | "./auth": { 22 | "import": "./dist/auth/index.js", 23 | "types": "./dist/auth/index.d.ts" 24 | }, 25 | "./firestore": { 26 | "import": "./dist/firestore/index.js", 27 | "types": "./dist/firestore/index.d.ts" 28 | }, 29 | "./data-connect": { 30 | "import": "./dist/data-connect/index.js", 31 | "types": "./dist/data-connect/index.d.ts" 32 | } 33 | }, 34 | "author": { 35 | "name": "Invertase", 36 | "email": "oss@invertase.io", 37 | "url": "https://github.com/invertase/tanstack-query-firebase" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/invertase/tanstack-query-firebase" 42 | }, 43 | "license": "Apache-2.0", 44 | "devDependencies": { 45 | "@dataconnect/default-connector": "file:../../dataconnect-sdk/js/default-connector", 46 | "@testing-library/react": "^16.0.1", 47 | "@types/react": "^19.0.1", 48 | "react": "^19.2.1" 49 | }, 50 | "peerDependencies": { 51 | "@tanstack/react-query": "^5", 52 | "firebase": "^11.3.0 || ^12.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/react/src/analytics/index.ts: -------------------------------------------------------------------------------- 1 | // useGetGoogleAnalyticsClientIdQuery 2 | // useLogEventMutation 3 | // useIsSupportedQuery 4 | -------------------------------------------------------------------------------- /packages/react/src/auth/index.ts: -------------------------------------------------------------------------------- 1 | // useAuthStateReadyQuery (Auth) 2 | // useConfirmationResultConfirmMutation (ConfirmationResult) 3 | // useUserDeleteMutation (User) 4 | // userUserGetIdTokenResultMutation (User) 5 | export { useGetIdTokenQuery } from "./useGetIdTokenQuery"; 6 | // useUserReloadMutation (User) 7 | // useVerifyPhoneNumberMutation (PhoneAuthProvider) 8 | // useMultiFactorUserEnrollMutation (MultiFactorUser) 9 | // useMultiFactorUserUnenrollMutation (MultiFactorUser) 10 | // useMultiFactorUserGetSessionMutation (MultiFactorUser) 11 | // useMultiFactorResolverResolveSignInMutation (MultiFactorResolver) 12 | // useApplyActionCodeMutation 13 | 14 | export { useApplyActionCodeMutation } from "./useApplyActionCodeMutation"; 15 | export { useCheckActionCodeMutation } from "./useCheckActionCodeMutation"; 16 | // useFetchSignInMethodsForEmailQuery 17 | export { useConfirmPasswordResetMutation } from "./useConfirmPasswordResetMutation"; 18 | // useCheckActionCodeMutation 19 | // useConfirmPasswordResetMutation 20 | export { useCreateUserWithEmailAndPasswordMutation } from "./useCreateUserWithEmailAndPasswordMutation"; 21 | // useDeleteUserMutation 22 | export { useDeleteUserMutation } from "./useDeleteUserMutation"; 23 | export { useGetRedirectResultQuery } from "./useGetRedirectResultQuery"; 24 | // useLinkWithCredentialMutation 25 | // useLinkWithPhoneNumberMutation 26 | // useLinkWithPopupMutation 27 | // useLinkWithRedirectMutation 28 | // useReauthenticateWithPhoneNumberMutation 29 | // useReauthenticateWithCredentialMutation 30 | // useReauthenticateWithPopupMutation 31 | // useReauthenticateWithRedirectMutation 32 | export { useReloadMutation } from "./useReloadMutation"; 33 | // useCreateUserWithEmailAndPasswordMutation 34 | // useGetRedirectResultQuery 35 | export { useRevokeAccessTokenMutation } from "./useRevokeAccessTokenMutation"; 36 | // useRevokeAccessTokenMutation 37 | // useSendPasswordResetEmailMutation 38 | export { useSendSignInLinkToEmailMutation } from "./useSendSignInLinkToEmailMutation"; 39 | export { useSignInAnonymouslyMutation } from "./useSignInAnonymouslyMutation"; 40 | export { useSignInWithCredentialMutation } from "./useSignInWithCredentialMutation"; 41 | // useSignInWithCustomTokenMutation 42 | export { useSignInWithEmailAndPasswordMutation } from "./useSignInWithEmailAndPasswordMutation"; 43 | // useSignInWithEmailLinkMutation 44 | // useSignInWithPhoneNumberMutation 45 | // useSignInWithPopupMutation 46 | // useSignInWithRedirectMutation 47 | export { useSignOutMutation } from "./useSignOutMutation"; 48 | // useUpdateCurrentUserMutation 49 | // useSignOutMutation 50 | export { useUpdateCurrentUserMutation } from "./useUpdateCurrentUserMutation"; 51 | // useValidatePasswordMutation 52 | // useVerifyPasswordResetCodeMutation 53 | export { useVerifyPasswordResetCodeMutation } from "./useVerifyPasswordResetCodeMutation"; 54 | // useSendEmailVerificationMutation 55 | // useUnlinkMutation 56 | // useUpdateEmailMutation 57 | // useUpdatePasswordMutation 58 | // useUpdateProfileMutation 59 | // useVerifyBeforeUpdateEmailMutation 60 | -------------------------------------------------------------------------------- /packages/react/src/auth/types.ts: -------------------------------------------------------------------------------- 1 | import type { UseMutationOptions } from "@tanstack/react-query"; 2 | 3 | export type AuthMutationOptions< 4 | TData = unknown, 5 | TError = Error, 6 | TVariables = void, 7 | > = Omit, "mutationFn">; 8 | -------------------------------------------------------------------------------- /packages/react/src/auth/useApplyActionCodeMutation.test.tsx: -------------------------------------------------------------------------------- 1 | import { act, renderHook, waitFor } from "@testing-library/react"; 2 | import { 3 | createUserWithEmailAndPassword, 4 | sendEmailVerification, 5 | } from "firebase/auth"; 6 | import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; 7 | import { auth, expectFirebaseError, wipeAuth } from "~/testing-utils"; 8 | import { queryClient, wrapper } from "../../utils"; 9 | import { useApplyActionCodeMutation } from "./useApplyActionCodeMutation"; 10 | import { waitForVerificationCode } from "./utils"; 11 | 12 | describe("useApplyActionCodeMutation", () => { 13 | const email = "tqf@invertase.io"; 14 | const password = "TanstackQueryFirebase#123"; 15 | 16 | beforeEach(async () => { 17 | queryClient.clear(); 18 | await wipeAuth(); 19 | await createUserWithEmailAndPassword(auth, email, password); 20 | }); 21 | 22 | afterEach(async () => { 23 | vi.clearAllMocks(); 24 | await auth.signOut(); 25 | }); 26 | 27 | test("successfully applies email verification action code", async () => { 28 | await sendEmailVerification(auth.currentUser!); 29 | const oobCode = await waitForVerificationCode(email); 30 | 31 | const { result } = renderHook(() => useApplyActionCodeMutation(auth), { 32 | wrapper, 33 | }); 34 | 35 | await act(async () => { 36 | await result.current.mutateAsync(oobCode!); 37 | }); 38 | 39 | await waitFor(() => expect(result.current.isSuccess).toBe(true)); 40 | }); 41 | 42 | test("handles invalid action code", async () => { 43 | const invalidCode = "invalid-action-code"; 44 | 45 | const { result } = renderHook(() => useApplyActionCodeMutation(auth), { 46 | wrapper, 47 | }); 48 | 49 | await act(async () => { 50 | try { 51 | await result.current.mutateAsync(invalidCode); 52 | } catch (error) { 53 | expectFirebaseError(error, "auth/invalid-action-code"); 54 | } 55 | }); 56 | 57 | await waitFor(() => expect(result.current.isError).toBe(true)); 58 | expect(result.current.error).toBeDefined(); 59 | expectFirebaseError(result.current.error, "auth/invalid-action-code"); 60 | }); 61 | 62 | test("handles empty action code", async () => { 63 | const { result } = renderHook(() => useApplyActionCodeMutation(auth), { 64 | wrapper, 65 | }); 66 | 67 | await act(async () => { 68 | try { 69 | await result.current.mutateAsync(""); 70 | } catch (error) { 71 | expectFirebaseError(error, "auth/invalid-req-type"); 72 | } 73 | }); 74 | 75 | await waitFor(() => expect(result.current.isError).toBe(true)); 76 | expect(result.current.error).toBeDefined(); 77 | expectFirebaseError(result.current.error, "auth/invalid-req-type"); 78 | }); 79 | 80 | test("executes onSuccess callback", async () => { 81 | await sendEmailVerification(auth.currentUser!); 82 | const oobCode = await waitForVerificationCode(email); 83 | const onSuccess = vi.fn(); 84 | 85 | const { result } = renderHook( 86 | () => useApplyActionCodeMutation(auth, { onSuccess }), 87 | { wrapper }, 88 | ); 89 | 90 | await act(async () => { 91 | await result.current.mutateAsync(oobCode!); 92 | }); 93 | 94 | await waitFor(() => expect(onSuccess).toHaveBeenCalled()); 95 | }); 96 | 97 | test("executes onError callback", async () => { 98 | const invalidCode = "invalid-action-code"; 99 | const onError = vi.fn(); 100 | 101 | const { result } = renderHook( 102 | () => useApplyActionCodeMutation(auth, { onError }), 103 | { wrapper }, 104 | ); 105 | 106 | await act(async () => { 107 | try { 108 | await result.current.mutateAsync(invalidCode); 109 | } catch (error) { 110 | expectFirebaseError(error, "auth/invalid-action-code"); 111 | } 112 | }); 113 | 114 | await waitFor(() => expect(onError).toHaveBeenCalled()); 115 | expect(onError.mock.calls[0][0]).toBeDefined(); 116 | expectFirebaseError(result.current.error, "auth/invalid-action-code"); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /packages/react/src/auth/useApplyActionCodeMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { type Auth, type AuthError, applyActionCode } from "firebase/auth"; 3 | 4 | type AuthUseMutationOptions< 5 | TData = unknown, 6 | TError = Error, 7 | TVariables = void, 8 | > = Omit, "mutationFn">; 9 | 10 | export function useApplyActionCodeMutation( 11 | auth: Auth, 12 | options?: AuthUseMutationOptions, 13 | ) { 14 | return useMutation({ 15 | ...options, 16 | mutationFn: (oobCode) => { 17 | return applyActionCode(auth, oobCode); 18 | }, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /packages/react/src/auth/useCheckActionCodeMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | type ActionCodeInfo, 4 | type Auth, 5 | type AuthError, 6 | checkActionCode, 7 | } from "firebase/auth"; 8 | 9 | type AuthUseMutationOptions< 10 | TData = unknown, 11 | TError = Error, 12 | TVariables = void, 13 | > = Omit, "mutationFn">; 14 | 15 | export function useCheckActionCodeMutation( 16 | auth: Auth, 17 | options?: AuthUseMutationOptions, 18 | ) { 19 | return useMutation({ 20 | ...options, 21 | mutationFn: (oobCode) => checkActionCode(auth, oobCode), 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /packages/react/src/auth/useConfirmPasswordResetMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { type Auth, type AuthError, confirmPasswordReset } from "firebase/auth"; 3 | 4 | type AuthUseMutationOptions< 5 | TData = unknown, 6 | TError = Error, 7 | TVariables = void, 8 | > = Omit, "mutationFn">; 9 | 10 | export function useConfirmPasswordResetMutation( 11 | auth: Auth, 12 | options?: AuthUseMutationOptions< 13 | void, 14 | AuthError, 15 | { oobCode: string; newPassword: string } 16 | >, 17 | ) { 18 | return useMutation( 19 | { 20 | ...options, 21 | mutationFn: ({ oobCode, newPassword }) => { 22 | return confirmPasswordReset(auth, oobCode, newPassword); 23 | }, 24 | }, 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /packages/react/src/auth/useCreateUserWithEmailAndPasswordMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | type Auth, 4 | type AuthError, 5 | createUserWithEmailAndPassword, 6 | type UserCredential, 7 | } from "firebase/auth"; 8 | 9 | type AuthUseMutationOptions< 10 | TData = unknown, 11 | TError = Error, 12 | TVariables = void, 13 | > = Omit, "mutationFn">; 14 | 15 | export function useCreateUserWithEmailAndPasswordMutation( 16 | auth: Auth, 17 | options?: AuthUseMutationOptions< 18 | UserCredential, 19 | AuthError, 20 | { email: string; password: string } 21 | >, 22 | ) { 23 | return useMutation< 24 | UserCredential, 25 | AuthError, 26 | { email: string; password: string } 27 | >({ 28 | ...options, 29 | mutationFn: ({ email, password }) => 30 | createUserWithEmailAndPassword(auth, email, password), 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /packages/react/src/auth/useDeleteUserMutation.test.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 | import { act, renderHook, waitFor } from "@testing-library/react"; 3 | import { createUserWithEmailAndPassword, type User } from "firebase/auth"; 4 | import type React from "react"; 5 | import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; 6 | import { auth, wipeAuth } from "~/testing-utils"; 7 | import { useDeleteUserMutation } from "./useDeleteUserMutation"; 8 | 9 | const queryClient = new QueryClient({ 10 | defaultOptions: { 11 | queries: { retry: false }, 12 | mutations: { retry: false }, 13 | }, 14 | }); 15 | 16 | const wrapper = ({ children }: { children: React.ReactNode }) => ( 17 | {children} 18 | ); 19 | 20 | describe("useVerifyPasswordResetCodeMutation", () => { 21 | const email = "tqf@invertase.io"; 22 | const password = "TanstackQueryFirebase#123"; 23 | let user: User; 24 | 25 | beforeEach(async () => { 26 | queryClient.clear(); 27 | await wipeAuth(); 28 | const userCredential = await createUserWithEmailAndPassword( 29 | auth, 30 | email, 31 | password, 32 | ); 33 | user = userCredential.user; 34 | }); 35 | 36 | afterEach(async () => { 37 | vi.clearAllMocks(); 38 | await auth.signOut(); 39 | }); 40 | 41 | test("successfully verifies the reset code", async () => { 42 | const { result } = renderHook(() => useDeleteUserMutation(auth), { 43 | wrapper, 44 | }); 45 | 46 | await act(async () => { 47 | result.current.mutate(user); 48 | }); 49 | 50 | await waitFor(() => expect(result.current.isSuccess).toBe(true)); 51 | 52 | expect(result.current.data).toBeUndefined(); 53 | }); 54 | 55 | test("resets mutation state correctly", async () => { 56 | const { result } = renderHook(() => useDeleteUserMutation(auth), { 57 | wrapper, 58 | }); 59 | 60 | act(() => { 61 | result.current.mutate(user); 62 | }); 63 | 64 | await waitFor(() => { 65 | expect(result.current.isSuccess).toBe(true); 66 | }); 67 | 68 | act(() => { 69 | result.current.reset(); 70 | }); 71 | 72 | await waitFor(() => { 73 | expect(result.current.isIdle).toBe(true); 74 | expect(result.current.data).toBeUndefined(); 75 | expect(result.current.error).toBeNull(); 76 | }); 77 | }); 78 | 79 | test("should call onSuccess when the user is successfully deleted", async () => { 80 | const onSuccess = vi.fn(); 81 | 82 | const { result } = renderHook( 83 | () => 84 | useDeleteUserMutation(auth, { 85 | onSuccess, 86 | }), 87 | { 88 | wrapper, 89 | }, 90 | ); 91 | 92 | act(() => { 93 | result.current.mutate(user); 94 | }); 95 | 96 | await waitFor(() => expect(result.current.isSuccess).toBe(true)); 97 | 98 | expect(onSuccess).toHaveBeenCalledTimes(1); 99 | expect(result.current.data).toBeUndefined(); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /packages/react/src/auth/useDeleteUserMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | type Auth, 4 | type AuthError, 5 | deleteUser, 6 | type User, 7 | } from "firebase/auth"; 8 | 9 | type AuthUMutationOptions< 10 | TData = unknown, 11 | TError = Error, 12 | TVariables = void, 13 | > = Omit, "mutationFn">; 14 | 15 | export function useDeleteUserMutation( 16 | _auth: Auth, 17 | options?: AuthUMutationOptions, 18 | ) { 19 | return useMutation({ 20 | ...options, 21 | mutationFn: (user: User) => deleteUser(user), 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /packages/react/src/auth/useGetIdTokenQuery.ts: -------------------------------------------------------------------------------- 1 | import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; 2 | import { type AuthError, getIdToken, type User } from "firebase/auth"; 3 | 4 | type AuthUseQueryOptions = Omit< 5 | UseQueryOptions, 6 | "queryFn" | "queryKey" 7 | > & { 8 | auth?: { 9 | forceRefresh?: boolean; 10 | }; 11 | }; 12 | 13 | const STALE_TIME = 55 * 60 * 1000; // Firebase tokens expire after 1 hour 14 | const GC_TIME = 60 * 60 * 1000; // Keep in cache for 1 hour 15 | 16 | const NO_USER_ERROR_MESSAGE = 17 | "[useGetIdTokenQuery] Cannot retrieve ID token: no Firebase user provided. Ensure a user is signed in before calling this hook."; 18 | 19 | // Query key factory for auth-related queries 20 | export const authQueryKeys = { 21 | all: ["auth"] as const, 22 | idToken: (userId: string | null, forceRefresh: boolean) => 23 | [...authQueryKeys.all, "idToken", { userId, forceRefresh }] as const, 24 | }; 25 | 26 | /** 27 | * Hook to get an ID token for a Firebase user 28 | * @param user - The Firebase User object (or null) 29 | * @param options - Query options including auth configuration 30 | * @returns TanStack Query result with the ID token 31 | * 32 | * @remarks 33 | * If you override the `enabled` option and set it to `true` while `user` is null, the query will run and immediately error. 34 | * This is allowed for advanced use cases, but is not recommended for most scenarios. 35 | * 36 | * @example 37 | * // Basic usage - gets cached token 38 | * const { data: token, isLoading } = useGetIdTokenQuery(user); 39 | * 40 | * // Force refresh the token 41 | * const { data: token } = useGetIdTokenQuery(user, { 42 | * auth: { forceRefresh: true } 43 | * }); 44 | * 45 | * // With additional query options 46 | * const { data: token, refetch } = useGetIdTokenQuery(user, { 47 | * enabled: !!user, 48 | * }); 49 | * 50 | * // Handle side effects with useEffect 51 | * useEffect(() => { 52 | * if (token) { 53 | * // Use token for API calls 54 | * api.setAuthToken(token); 55 | * } 56 | * }, [token]); 57 | * 58 | * // Manually re-fetch token (respects the initial forceRefresh option) 59 | * const { refetch } = useGetIdTokenQuery(user); 60 | * const handleRefetch = () => refetch(); 61 | * 62 | * // For actual force refresh, use a separate query with forceRefresh: true 63 | * const { data: freshToken, refetch: refetchFresh } = useGetIdTokenQuery(user, { 64 | * auth: { forceRefresh: true }, 65 | * enabled: false, // Manual trigger only 66 | * }); 67 | * const handleForceRefresh = () => refetchFresh(); 68 | */ 69 | export function useGetIdTokenQuery( 70 | user: User | null, 71 | options?: AuthUseQueryOptions, 72 | ) { 73 | const { auth: authOptions, ...queryOptions } = options || {}; 74 | const forceRefresh = authOptions?.forceRefresh ?? false; 75 | 76 | const queryKey = authQueryKeys.idToken(user?.uid ?? null, forceRefresh); 77 | 78 | const queryFn = () => 79 | user 80 | ? getIdToken(user, forceRefresh) 81 | : Promise.reject(new Error(NO_USER_ERROR_MESSAGE)); 82 | 83 | return useQuery({ 84 | ...queryOptions, 85 | queryKey, 86 | queryFn, 87 | staleTime: forceRefresh ? 0 : STALE_TIME, 88 | gcTime: GC_TIME, 89 | enabled: options?.enabled !== undefined ? options.enabled : !!user, 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /packages/react/src/auth/useGetRedirectResultQuery.ts: -------------------------------------------------------------------------------- 1 | import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; 2 | import { 3 | type Auth, 4 | type AuthError, 5 | getRedirectResult, 6 | type PopupRedirectResolver, 7 | type UserCredential, 8 | } from "firebase/auth"; 9 | 10 | type AuthUseQueryOptions = Omit< 11 | UseQueryOptions, 12 | "queryFn" 13 | > & { auth?: { resolver?: PopupRedirectResolver } }; 14 | 15 | export function useGetRedirectResultQuery( 16 | auth: Auth, 17 | options: AuthUseQueryOptions, 18 | ) { 19 | const { auth: authOptions, ...queryOptions } = options; 20 | const resolver = authOptions?.resolver; 21 | 22 | return useQuery({ 23 | ...queryOptions, 24 | queryFn: () => getRedirectResult(auth, resolver), 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /packages/react/src/auth/useReloadMutation.test.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 | import { act, renderHook, waitFor } from "@testing-library/react"; 3 | import { 4 | createUserWithEmailAndPassword, 5 | signInWithEmailAndPassword, 6 | type User, 7 | } from "firebase/auth"; 8 | import type React from "react"; 9 | import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; 10 | import { auth, wipeAuth } from "~/testing-utils"; 11 | import { useReloadMutation } from "./useReloadMutation"; 12 | 13 | const queryClient = new QueryClient({ 14 | defaultOptions: { 15 | queries: { retry: false }, 16 | mutations: { retry: false }, 17 | }, 18 | }); 19 | 20 | const wrapper = ({ children }: { children: React.ReactNode }) => ( 21 | {children} 22 | ); 23 | 24 | describe("useReloadMutation", () => { 25 | const email = "tqf@invertase.io"; 26 | const password = "TanstackQueryFirebase#123"; 27 | let user: User; 28 | beforeEach(async () => { 29 | queryClient.clear(); 30 | await wipeAuth(); 31 | const userCredential = await createUserWithEmailAndPassword( 32 | auth, 33 | email, 34 | password, 35 | ); 36 | user = userCredential.user; 37 | }); 38 | 39 | afterEach(async () => { 40 | vi.clearAllMocks(); 41 | await auth.signOut(); 42 | }); 43 | 44 | test.sequential("should successfully reloads user data", async () => { 45 | await signInWithEmailAndPassword(auth, email, password); 46 | 47 | const { result } = renderHook(() => useReloadMutation(), { wrapper }); 48 | 49 | act(() => result.current.mutate(user)); 50 | 51 | await waitFor(() => expect(result.current.isSuccess).toBe(true)); 52 | }); 53 | 54 | test("should handle onSuccess callback", async () => { 55 | await signInWithEmailAndPassword(auth, email, password); 56 | 57 | const onSuccess = vi.fn(); 58 | const { result } = renderHook(() => useReloadMutation({ onSuccess }), { 59 | wrapper, 60 | }); 61 | 62 | act(() => { 63 | result.current.mutate(user); 64 | }); 65 | 66 | await waitFor(() => expect(result.current.isSuccess).toBe(true)); 67 | 68 | expect(onSuccess).toHaveBeenCalled(); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/react/src/auth/useReloadMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { type AuthError, reload, type User } from "firebase/auth"; 3 | 4 | type AuthMutationOptions< 5 | TData = unknown, 6 | TError = Error, 7 | TVariables = void, 8 | > = Omit, "mutationFn">; 9 | 10 | export function useReloadMutation( 11 | options?: AuthMutationOptions, 12 | ) { 13 | return useMutation({ 14 | ...options, 15 | mutationFn: (user: User) => reload(user), 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /packages/react/src/auth/useRevokeAccessTokenMutation.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@tanstack/react-query"; 2 | import { type Auth, type AuthError, revokeAccessToken } from "firebase/auth"; 3 | import type { AuthMutationOptions } from "./types"; 4 | 5 | export function useRevokeAccessTokenMutation( 6 | auth: Auth, 7 | options?: AuthMutationOptions, 8 | ) { 9 | return useMutation({ 10 | ...options, 11 | mutationFn: (token: string) => revokeAccessToken(auth, token), 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /packages/react/src/auth/useSendSignInLinkToEmailMutation.test.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 | import { act, renderHook, waitFor } from "@testing-library/react"; 3 | import type React from "react"; 4 | import { beforeEach, describe, expect, test } from "vitest"; 5 | import { auth, wipeAuth } from "~/testing-utils"; 6 | import { useSendSignInLinkToEmailMutation } from "./useSendSignInLinkToEmailMutation"; 7 | 8 | const queryClient = new QueryClient({ 9 | defaultOptions: { 10 | queries: { retry: false }, 11 | mutations: { retry: false }, 12 | }, 13 | }); 14 | 15 | const wrapper = ({ children }: { children: React.ReactNode }) => ( 16 | {children} 17 | ); 18 | 19 | describe("useSendSignInLinkToEmailMutation", () => { 20 | const email = "tanstack-query-firebase@invertase.io"; 21 | const actionCodeSettings = { 22 | url: `https://invertase.io/?email=${email}`, 23 | iOS: { 24 | bundleId: "com.example.ios", 25 | }, 26 | android: { 27 | packageName: "com.example.android", 28 | installApp: true, 29 | minimumVersion: "12", 30 | }, 31 | handleCodeInApp: true, 32 | }; 33 | 34 | beforeEach(async () => { 35 | queryClient.clear(); 36 | await wipeAuth(); 37 | }); 38 | 39 | test("resets mutation state correctly", async () => { 40 | const { result } = renderHook( 41 | () => useSendSignInLinkToEmailMutation(auth), 42 | { wrapper }, 43 | ); 44 | 45 | act(() => { 46 | result.current.mutate({ email, actionCodeSettings }); 47 | }); 48 | 49 | await waitFor(() => { 50 | expect(result.current.isSuccess).toBe(true); 51 | }); 52 | 53 | act(() => { 54 | result.current.reset(); 55 | }); 56 | 57 | await waitFor(() => { 58 | expect(result.current.isIdle).toBe(true); 59 | expect(result.current.data).toBeUndefined(); 60 | expect(result.current.error).toBeNull(); 61 | }); 62 | }); 63 | 64 | test("successfully sends sign-in link to email", async () => { 65 | const { result } = renderHook( 66 | () => useSendSignInLinkToEmailMutation(auth), 67 | { wrapper }, 68 | ); 69 | 70 | act(() => { 71 | result.current.mutate({ email, actionCodeSettings }); 72 | }); 73 | 74 | await waitFor(() => { 75 | expect(result.current.isSuccess).toBe(true); 76 | }); 77 | 78 | expect(result.current.isSuccess).toBe(true); 79 | expect(result.current.error).toBeNull(); 80 | }); 81 | 82 | test("allows multiple sequential send attempts", async () => { 83 | const { result } = renderHook( 84 | () => useSendSignInLinkToEmailMutation(auth), 85 | { wrapper }, 86 | ); 87 | 88 | // First attempt 89 | act(() => { 90 | result.current.mutate({ email, actionCodeSettings }); 91 | }); 92 | 93 | await waitFor(() => { 94 | expect(result.current.isSuccess).toBe(true); 95 | }); 96 | 97 | // Reset state 98 | act(() => { 99 | result.current.reset(); 100 | }); 101 | 102 | await waitFor(() => { 103 | expect(result.current.isIdle).toBe(true); 104 | expect(result.current.data).toBeUndefined(); 105 | expect(result.current.error).toBeNull(); 106 | }); 107 | 108 | // Second attempt 109 | act(() => { 110 | result.current.mutate({ email, actionCodeSettings }); 111 | }); 112 | 113 | await waitFor(() => { 114 | expect(result.current.isSuccess).toBe(true); 115 | }); 116 | 117 | expect(result.current.error).toBeNull(); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /packages/react/src/auth/useSendSignInLinkToEmailMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | type ActionCodeSettings, 4 | type Auth, 5 | type AuthError, 6 | sendSignInLinkToEmail, 7 | } from "firebase/auth"; 8 | 9 | type SendSignInLinkParams = { 10 | email: string; 11 | actionCodeSettings: ActionCodeSettings; 12 | }; 13 | 14 | type AuthUseMutationOptions< 15 | TData = unknown, 16 | TError = Error, 17 | TVariables = void, 18 | > = Omit, "mutationFn">; 19 | 20 | export function useSendSignInLinkToEmailMutation( 21 | auth: Auth, 22 | options?: AuthUseMutationOptions, 23 | ) { 24 | return useMutation({ 25 | ...options, 26 | mutationFn: ({ email, actionCodeSettings }) => 27 | sendSignInLinkToEmail(auth, email, actionCodeSettings), 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 | import { act, renderHook, waitFor } from "@testing-library/react"; 3 | import { afterEach, beforeEach, describe, expect, test } from "vitest"; 4 | import { auth, wipeAuth } from "~/testing-utils"; 5 | import { useSignInAnonymouslyMutation } from "./useSignInAnonymouslyMutation"; 6 | 7 | const queryClient = new QueryClient({ 8 | defaultOptions: { 9 | queries: { retry: false }, 10 | mutations: { retry: false }, 11 | }, 12 | }); 13 | 14 | const wrapper = ({ children }: { children: React.ReactNode }) => ( 15 | {children} 16 | ); 17 | 18 | describe("useSignInAnonymouslyMutation", () => { 19 | beforeEach(async () => { 20 | queryClient.clear(); 21 | await wipeAuth(); 22 | }); 23 | 24 | afterEach(async () => { 25 | await auth.signOut(); 26 | }); 27 | 28 | test("successfully signs in anonymously", async () => { 29 | const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { 30 | wrapper, 31 | }); 32 | 33 | act(() => { 34 | result.current.mutate(); 35 | }); 36 | 37 | await waitFor(() => { 38 | expect(result.current.isSuccess).toBe(true); 39 | }); 40 | 41 | expect(result.current.data?.user.isAnonymous).toBe(true); 42 | }); 43 | 44 | test("resets mutation state correctly", async () => { 45 | const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { 46 | wrapper, 47 | }); 48 | 49 | act(() => { 50 | result.current.mutateAsync(); 51 | }); 52 | 53 | await waitFor(() => { 54 | expect(result.current.data?.user.isAnonymous).toBe(true); 55 | expect(result.current.isSuccess).toBe(true); 56 | }); 57 | 58 | act(() => { 59 | result.current.reset(); 60 | }); 61 | 62 | await waitFor(() => { 63 | expect(result.current.isIdle).toBe(true); 64 | expect(result.current.data).toBeUndefined(); 65 | expect(result.current.error).toBeNull(); 66 | }); 67 | }); 68 | 69 | test("allows multiple sequential sign-ins", async () => { 70 | const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { 71 | wrapper, 72 | }); 73 | 74 | // First sign-in 75 | act(() => { 76 | result.current.mutate(); 77 | }); 78 | 79 | await waitFor(() => { 80 | expect(result.current.isSuccess).toBe(true); 81 | expect(result.current.data?.user.isAnonymous).toBe(true); 82 | }); 83 | 84 | // Reset state 85 | act(() => { 86 | result.current.reset(); 87 | }); 88 | 89 | await waitFor(() => { 90 | expect(result.current.isIdle).toBe(true); 91 | expect(result.current.data).toBeUndefined(); 92 | expect(result.current.error).toBeNull(); 93 | }); 94 | 95 | // Second sign-in 96 | act(() => { 97 | result.current.mutate(); 98 | }); 99 | 100 | await waitFor(() => { 101 | expect(result.current.isSuccess).toBe(true); 102 | expect(result.current.data?.user.isAnonymous).toBe(true); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /packages/react/src/auth/useSignInAnonymouslyMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | type Auth, 4 | type AuthError, 5 | signInAnonymously, 6 | type UserCredential, 7 | } from "firebase/auth"; 8 | 9 | type SignInAnonymouslyOptions = Omit< 10 | UseMutationOptions, 11 | "mutationFn" 12 | >; 13 | 14 | export function useSignInAnonymouslyMutation( 15 | auth: Auth, 16 | options?: SignInAnonymouslyOptions, 17 | ) { 18 | return useMutation({ 19 | ...options, 20 | mutationFn: () => signInAnonymously(auth), 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /packages/react/src/auth/useSignInWithCredentialMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | type Auth, 4 | type AuthCredential, 5 | type AuthError, 6 | signInWithCredential, 7 | type UserCredential, 8 | } from "firebase/auth"; 9 | 10 | type AuthUseMutationOptions = Omit< 11 | UseMutationOptions, 12 | "mutationFn" 13 | >; 14 | 15 | export function useSignInWithCredentialMutation( 16 | auth: Auth, 17 | credential: AuthCredential, 18 | options?: AuthUseMutationOptions, 19 | ) { 20 | return useMutation({ 21 | ...options, 22 | mutationFn: () => signInWithCredential(auth, credential), 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /packages/react/src/auth/useSignInWithEmailAndPasswordMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | type Auth, 4 | type AuthError, 5 | signInWithEmailAndPassword, 6 | type UserCredential, 7 | } from "firebase/auth"; 8 | 9 | type AuthUseMutationOptions< 10 | TData = unknown, 11 | TError = Error, 12 | TVariables = void, 13 | > = Omit, "mutationFn">; 14 | 15 | export function useSignInWithEmailAndPasswordMutation( 16 | auth: Auth, 17 | options?: AuthUseMutationOptions< 18 | UserCredential, 19 | AuthError, 20 | { email: string; password: string } 21 | >, 22 | ) { 23 | return useMutation< 24 | UserCredential, 25 | AuthError, 26 | { email: string; password: string } 27 | >({ 28 | ...options, 29 | mutationFn: ({ email, password }) => 30 | signInWithEmailAndPassword(auth, email, password), 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /packages/react/src/auth/useSignOutMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { type Auth, signOut } from "firebase/auth"; 3 | 4 | type AuthUseMutationOptions< 5 | TData = unknown, 6 | TError = Error, 7 | TVariables = void, 8 | > = Omit, "mutationFn">; 9 | 10 | export function useSignOutMutation( 11 | auth: Auth, 12 | options?: AuthUseMutationOptions, 13 | ) { 14 | return useMutation({ 15 | ...options, 16 | mutationFn: () => signOut(auth), 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /packages/react/src/auth/useUpdateCurrentUserMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | type Auth, 4 | type AuthError, 5 | type User, 6 | updateCurrentUser, 7 | } from "firebase/auth"; 8 | 9 | type AuthUseMutationOptions< 10 | TData = unknown, 11 | TError = Error, 12 | TVariables = void, 13 | > = Omit, "mutationFn">; 14 | 15 | export function useUpdateCurrentUserMutation( 16 | auth: Auth, 17 | options?: AuthUseMutationOptions, 18 | ) { 19 | return useMutation({ 20 | ...options, 21 | mutationFn: (user) => updateCurrentUser(auth, user), 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /packages/react/src/auth/useVerifyPasswordResetCodeMutation.test.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 | import { act, renderHook, waitFor } from "@testing-library/react"; 3 | import { 4 | createUserWithEmailAndPassword, 5 | sendPasswordResetEmail, 6 | } from "firebase/auth"; 7 | import type React from "react"; 8 | import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; 9 | import { auth, wipeAuth } from "~/testing-utils"; 10 | import { useVerifyPasswordResetCodeMutation } from "./useVerifyPasswordResetCodeMutation"; 11 | import { waitForPasswordResetCode } from "./utils"; 12 | 13 | const queryClient = new QueryClient({ 14 | defaultOptions: { 15 | queries: { retry: false }, 16 | mutations: { retry: false }, 17 | }, 18 | }); 19 | 20 | const wrapper = ({ children }: { children: React.ReactNode }) => ( 21 | {children} 22 | ); 23 | 24 | describe("useVerifyPasswordResetCodeMutation", () => { 25 | const email = "tqf@invertase.io"; 26 | const password = "TanstackQueryFirebase#123"; 27 | 28 | beforeEach(async () => { 29 | queryClient.clear(); 30 | await wipeAuth(); 31 | await createUserWithEmailAndPassword(auth, email, password); 32 | await sendPasswordResetEmail(auth, email); 33 | }); 34 | 35 | afterEach(async () => { 36 | vi.clearAllMocks(); 37 | await auth.signOut(); 38 | }); 39 | 40 | test("successfully verifies the reset code", async () => { 41 | const code = await waitForPasswordResetCode(email); 42 | 43 | const { result } = renderHook( 44 | () => useVerifyPasswordResetCodeMutation(auth), 45 | { 46 | wrapper, 47 | }, 48 | ); 49 | 50 | await act(async () => { 51 | code && (await result.current.mutateAsync(code)); 52 | }); 53 | 54 | await waitFor(() => expect(result.current.isSuccess).toBe(true)); 55 | 56 | expect(result.current.data).toBe(email); 57 | expect(result.current.variables).toBe(code); 58 | }); 59 | 60 | test("handles invalid reset code", async () => { 61 | const invalidCode = "invalid-reset-code"; 62 | 63 | const { result } = renderHook( 64 | () => useVerifyPasswordResetCodeMutation(auth), 65 | { wrapper }, 66 | ); 67 | 68 | await act(async () => { 69 | await result.current.mutate(invalidCode); 70 | }); 71 | 72 | await waitFor(() => expect(result.current.isError).toBe(true)); 73 | 74 | expect(result.current.error).toBeDefined(); 75 | // TODO: Assert Firebase error for auth/invalid-action-code 76 | }); 77 | 78 | test("handles empty reset code", async () => { 79 | const emptyCode = ""; 80 | 81 | const { result } = renderHook( 82 | () => useVerifyPasswordResetCodeMutation(auth), 83 | { wrapper }, 84 | ); 85 | 86 | await act(async () => await result.current.mutate(emptyCode)); 87 | 88 | await waitFor(() => expect(result.current.isError).toBe(true)); 89 | 90 | expect(result.current.error).toBeDefined(); 91 | // TODO: Assert Firebase error for auth/invalid-action-code 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /packages/react/src/auth/useVerifyPasswordResetCodeMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | type Auth, 4 | type AuthError, 5 | verifyPasswordResetCode, 6 | } from "firebase/auth"; 7 | 8 | type AuthUseMutationOptions< 9 | TData = unknown, 10 | TError = Error, 11 | TVariables = void, 12 | > = Omit, "mutationFn">; 13 | 14 | export function useVerifyPasswordResetCodeMutation( 15 | auth: Auth, 16 | options?: AuthUseMutationOptions, 17 | ) { 18 | return useMutation({ 19 | ...options, 20 | mutationFn: (code: string) => verifyPasswordResetCode(auth, code), 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /packages/react/src/auth/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | 4 | /** 5 | * Reads the Firebase emulator debug log and extracts a specific code from the logs. 6 | * @param email The email address for which the code was requested. 7 | * @param logPattern A regular expression pattern to match the log entry. 8 | * @param extractCodeFn A function to extract the code from the relevant log line. 9 | * @returns The extracted code or null if not found. 10 | */ 11 | async function getCodeFromLogs( 12 | _email: string, 13 | logPattern: RegExp, 14 | extractCodeFn: (line: string) => string | null, 15 | ): Promise { 16 | try { 17 | // Read the firebase-debug.log file 18 | const logPath = path.join(process.cwd(), "../../firebase-debug.log"); 19 | const logContent = await fs.promises.readFile(logPath, "utf8"); 20 | 21 | // Reverse lines to start with the most recent logs 22 | const lines = logContent.split("\n").reverse(); 23 | 24 | for (const line of lines) { 25 | if (logPattern.test(line)) { 26 | const code = extractCodeFn(line); 27 | if (code) { 28 | return code; 29 | } 30 | } 31 | } 32 | 33 | return null; 34 | } catch (error) { 35 | console.error("Error reading Firebase debug log:", error); 36 | return null; 37 | } 38 | } 39 | 40 | /** 41 | * Waits for a specific code to appear in the logs. 42 | * @param email The email address for which the code was requested. 43 | * @param logPattern A regular expression pattern to match the log entry. 44 | * @param extractCodeFn A function to extract the code from the relevant log line. 45 | * @param timeout Maximum time to wait in milliseconds. 46 | * @param interval Interval between checks in milliseconds. 47 | * @returns The extracted code or null if timeout is reached. 48 | */ 49 | async function waitForCode( 50 | email: string, 51 | logPattern: RegExp, 52 | extractCodeFn: (line: string) => string | null, 53 | timeout = 5000, 54 | interval = 100, 55 | ): Promise { 56 | const startTime = Date.now(); 57 | 58 | while (Date.now() - startTime < timeout) { 59 | const code = await getCodeFromLogs(email, logPattern, extractCodeFn); 60 | if (code) { 61 | return code; 62 | } 63 | await new Promise((resolve) => setTimeout(resolve, interval)); 64 | } 65 | 66 | return null; 67 | } 68 | 69 | /** 70 | * Extracts the oobCode from a log line. 71 | * @param line The log line containing the oobCode link. 72 | * @returns The oobCode or null if not found. 73 | */ 74 | function extractOobCode(line: string): string | null { 75 | const url = line.match( 76 | /http:\/\/127\.0\.0\.1:9099\/emulator\/action\?.*?$/, 77 | )?.[0]; 78 | return url ? new URL(url).searchParams.get("oobCode") : null; 79 | } 80 | 81 | export async function waitForPasswordResetCode( 82 | email: string, 83 | timeout = 5000, 84 | interval = 100, 85 | ): Promise { 86 | const logPattern = new RegExp( 87 | `To reset the password for ${email.replace( 88 | ".", 89 | "\\.", 90 | )}.*?http://127\\.0\\.0\\.1:9099.*`, 91 | "i", 92 | ); 93 | return waitForCode(email, logPattern, extractOobCode, timeout, interval); 94 | } 95 | 96 | export async function waitForVerificationCode( 97 | email: string, 98 | timeout = 5000, 99 | interval = 100, 100 | ): Promise { 101 | const logPattern = new RegExp( 102 | `To verify the email address ${email.replace( 103 | ".", 104 | "\\.", 105 | )}.*?http://127\\.0\\.0\\.1:9099.*`, 106 | "i", 107 | ); 108 | return waitForCode(email, logPattern, extractOobCode, timeout, interval); 109 | } 110 | -------------------------------------------------------------------------------- /packages/react/src/data-connect/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | DataConnectQueryClient, 3 | type DataConnectQueryOptions, 4 | } from "./query-client"; 5 | export type { 6 | QueryResultRequiredRef, 7 | UseDataConnectMutationResult, 8 | UseDataConnectQueryResult, 9 | } from "./types"; 10 | export { 11 | useDataConnectMutation, 12 | type useDataConnectMutationOptions, 13 | } from "./useDataConnectMutation"; 14 | export { 15 | useDataConnectQuery, 16 | type useDataConnectQueryOptions, 17 | } from "./useDataConnectQuery"; 18 | export { validateReactArgs } from "./validateReactArgs"; 19 | -------------------------------------------------------------------------------- /packages/react/src/data-connect/query-client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type FetchQueryOptions, 3 | QueryClient, 4 | type QueryKey, 5 | } from "@tanstack/react-query"; 6 | import type { FirebaseError } from "firebase/app"; 7 | import { 8 | executeQuery, 9 | type QueryRef, 10 | type QueryResult, 11 | } from "firebase/data-connect"; 12 | 13 | export type DataConnectQueryOptions = Omit< 14 | FetchQueryOptions, 15 | "queryFn" | "queryKey" 16 | > & { 17 | queryRef: QueryRef; 18 | queryKey?: QueryKey; 19 | }; 20 | 21 | export class DataConnectQueryClient extends QueryClient { 22 | prefetchDataConnectQuery, Variables>( 23 | refOrResult: QueryRef | QueryResult, 24 | options?: DataConnectQueryOptions, 25 | ) { 26 | let queryRef: QueryRef; 27 | let initialData: Data | undefined; 28 | 29 | if ("ref" in refOrResult) { 30 | queryRef = refOrResult.ref; 31 | initialData = { 32 | ...refOrResult.data, 33 | ref: refOrResult.ref, 34 | source: refOrResult.source, 35 | fetchTime: refOrResult.fetchTime, 36 | }; 37 | } else { 38 | queryRef = refOrResult; 39 | } 40 | 41 | return this.prefetchQuery({ 42 | ...options, 43 | initialData, 44 | queryKey: options?.queryKey ?? [ 45 | queryRef.name, 46 | queryRef.variables || null, 47 | ], 48 | queryFn: async () => { 49 | const response = await executeQuery(queryRef); 50 | 51 | const data = { 52 | ...response.data, 53 | ref: response.ref, 54 | source: response.source, 55 | fetchTime: response.fetchTime, 56 | }; 57 | 58 | // Ensures no serialization issues with undefined values 59 | return JSON.parse(JSON.stringify(data)); 60 | }, 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/react/src/data-connect/types.ts: -------------------------------------------------------------------------------- 1 | import type { UseMutationResult, UseQueryResult } from "@tanstack/react-query"; 2 | import type { FirebaseError } from "firebase/app"; 3 | import type { MutationResult, QueryResult } from "firebase/data-connect"; 4 | 5 | export type QueryResultRequiredRef = Partial< 6 | QueryResult 7 | > & 8 | Required, "ref">>; 9 | 10 | export type UseDataConnectQueryResult = UseQueryResult< 11 | Data, 12 | FirebaseError 13 | > & { 14 | dataConnectResult?: QueryResultRequiredRef; 15 | }; 16 | 17 | export type UseDataConnectMutationResult = UseMutationResult< 18 | Data, 19 | FirebaseError, 20 | Variables 21 | > & { 22 | dataConnectResult?: Partial>; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/react/src/data-connect/useDataConnectMutation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type UseMutationOptions, 3 | useMutation, 4 | useQueryClient, 5 | } from "@tanstack/react-query"; 6 | import type { FirebaseError } from "firebase/app"; 7 | import { 8 | type CallerSdkType, 9 | CallerSdkTypeEnum, 10 | type DataConnect, 11 | executeMutation, 12 | type MutationRef, 13 | type MutationResult, 14 | type QueryRef, 15 | } from "firebase/data-connect"; 16 | import { useState } from "react"; 17 | import type { UseDataConnectMutationResult } from "./types"; 18 | 19 | export type useDataConnectMutationOptions< 20 | TData = unknown, 21 | TError = FirebaseError, 22 | Variables = unknown, 23 | > = Omit, "mutationFn"> & { 24 | invalidate?: ReadonlyArray< 25 | QueryRef | (() => QueryRef) 26 | >; 27 | }; 28 | 29 | export function useDataConnectMutation< 30 | Fn extends 31 | | (() => MutationRef) 32 | | ((vars: any) => MutationRef), 33 | Data = ReturnType< 34 | Fn extends () => MutationRef 35 | ? () => MutationRef 36 | : Fn extends (vars: any) => MutationRef 37 | ? (vars: any) => MutationRef 38 | : Fn 39 | > extends MutationRef 40 | ? D 41 | : never, 42 | Variables = Fn extends () => MutationRef 43 | ? void 44 | : Fn extends (vars: infer V) => MutationRef 45 | ? V 46 | : Fn extends (dc: DataConnect, vars: infer V) => MutationRef 47 | ? V 48 | : never, 49 | >( 50 | ref: Fn, 51 | options?: useDataConnectMutationOptions, 52 | _callerSdkType: CallerSdkType = CallerSdkTypeEnum.TanstackReactCore, 53 | ): UseDataConnectMutationResult { 54 | const queryClient = useQueryClient(); 55 | const [dataConnectResult, setDataConnectResult] = useState< 56 | MutationResult | undefined 57 | >(undefined); 58 | const originalResult = useMutation({ 59 | ...options, 60 | onSuccess(...args) { 61 | if (options?.invalidate?.length) { 62 | for (const ref of options.invalidate) { 63 | if ("variables" in ref && ref.variables !== undefined) { 64 | queryClient.invalidateQueries({ 65 | queryKey: [ref.name, ref.variables], 66 | exact: true, 67 | }); 68 | } else { 69 | queryClient.invalidateQueries({ 70 | queryKey: [ref.name], 71 | }); 72 | } 73 | } 74 | } 75 | 76 | options?.onSuccess?.(...args); 77 | }, 78 | mutationFn: async (variables) => { 79 | const mutationRef = typeof ref === "function" ? ref(variables) : ref; 80 | 81 | // @ts-expect-error function is hidden under `DataConnect`. 82 | mutationRef.dataConnect._setCallerSdkType(_callerSdkType); 83 | const response = await executeMutation(mutationRef); 84 | 85 | setDataConnectResult(response); 86 | return { 87 | ...response.data, 88 | }; 89 | }, 90 | }); 91 | return { 92 | dataConnectResult, 93 | ...originalResult, 94 | }; 95 | } 96 | -------------------------------------------------------------------------------- /packages/react/src/data-connect/useDataConnectQuery.ts: -------------------------------------------------------------------------------- 1 | import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; 2 | import type { FirebaseError } from "firebase/app"; 3 | import { 4 | type CallerSdkType, 5 | CallerSdkTypeEnum, 6 | executeQuery, 7 | type QueryRef, 8 | type QueryResult, 9 | } from "firebase/data-connect"; 10 | import { useEffect, useState } from "react"; 11 | import type { PartialBy } from "../../utils"; 12 | import type { 13 | QueryResultRequiredRef, 14 | UseDataConnectQueryResult, 15 | } from "./types"; 16 | import { deepEqual } from "./utils"; 17 | 18 | export type useDataConnectQueryOptions< 19 | TData = object, 20 | TError = FirebaseError, 21 | > = PartialBy, "queryFn">, "queryKey">; 22 | function getRef( 23 | refOrResult: QueryRef | QueryResult, 24 | ): QueryRef { 25 | return "ref" in refOrResult ? refOrResult.ref : refOrResult; 26 | } 27 | 28 | export function useDataConnectQuery( 29 | refOrResult: QueryRef | QueryResult, 30 | options?: useDataConnectQueryOptions, 31 | _callerSdkType: CallerSdkType = CallerSdkTypeEnum.TanstackReactCore, 32 | ): UseDataConnectQueryResult { 33 | const [dataConnectResult, setDataConnectResult] = useState< 34 | QueryResultRequiredRef 35 | >("ref" in refOrResult ? refOrResult : { ref: refOrResult }); 36 | const [ref, setRef] = useState(dataConnectResult.ref); 37 | // TODO(mtewani): in the future we should allow for users to pass in `QueryResult` objects into `initialData`. 38 | const [initialData] = useState( 39 | dataConnectResult.data || options?.initialData, 40 | ); 41 | 42 | useEffect(() => { 43 | setRef((oldRef) => { 44 | const newRef = getRef(refOrResult); 45 | if ( 46 | newRef.name !== oldRef.name || 47 | !deepEqual(oldRef.variables, newRef.variables) 48 | ) { 49 | return newRef; 50 | } 51 | return oldRef; 52 | }); 53 | }, [refOrResult]); 54 | 55 | // @ts-expect-error function is hidden under `DataConnect`. 56 | ref.dataConnect._setCallerSdkType(_callerSdkType); 57 | const useQueryResult = useQuery({ 58 | ...options, 59 | initialData, 60 | queryKey: options?.queryKey ?? [ref.name, ref.variables || null], 61 | queryFn: async () => { 62 | const response = await executeQuery(ref); 63 | setDataConnectResult(response); 64 | return { 65 | ...response.data, 66 | }; 67 | }, 68 | }); 69 | return { 70 | ...useQueryResult, 71 | dataConnectResult, 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /packages/react/src/data-connect/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { deepEqual } from "./utils"; 3 | 4 | describe("utils", () => { 5 | test("should compare native types correctly", () => { 6 | const nonEqualNumbers = deepEqual(1, 2); 7 | expect(nonEqualNumbers).to.eq(false); 8 | const equalNumbers = deepEqual(2, 2); 9 | expect(equalNumbers).to.eq(true); 10 | const nonEqualBools = deepEqual(false, true); 11 | expect(nonEqualBools).to.eq(false); 12 | const equalBools = deepEqual(false, false); 13 | expect(equalBools).to.eq(true); 14 | const nonEqualStrings = deepEqual("a", "b"); 15 | expect(nonEqualStrings).to.eq(false); 16 | const equalStrings = deepEqual("a", "a"); 17 | expect(equalStrings).to.eq(true); 18 | }); 19 | test("should compare object types correctly", () => { 20 | const equalNulls = deepEqual(null, null); 21 | expect(equalNulls).to.eq(true); 22 | const nonEqualNulls = deepEqual(null, {}); 23 | expect(nonEqualNulls).to.eq(false); 24 | const nonEqualObjects = deepEqual({}, { a: "b" }); 25 | expect(nonEqualObjects).to.eq(false); 26 | const equalObjects = deepEqual({ a: "b" }, { a: "b" }); 27 | expect(equalObjects).to.eq(true); 28 | const equalObjectsWithOrder = deepEqual( 29 | { a: "b", b: "c" }, 30 | { b: "c", a: "b" }, 31 | ); 32 | expect(equalObjectsWithOrder).to.eq(true); 33 | const nestedObjects = deepEqual( 34 | { a: { movie_insert: "b" } }, 35 | { a: { movie_insert: "c" } }, 36 | ); 37 | expect(nestedObjects).to.eq(false); 38 | }); 39 | test("should compare arrays correctly", () => { 40 | const emptyArrays = deepEqual([], []); 41 | expect(emptyArrays).to.eq(true); 42 | const nonEmptyArrays = deepEqual([1], [1]); 43 | expect(nonEmptyArrays).to.eq(true); 44 | const nonEmptyDiffArrays = deepEqual([2], [1]); 45 | expect(nonEmptyDiffArrays).to.eq(false); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/react/src/data-connect/utils.ts: -------------------------------------------------------------------------------- 1 | export function deepEqual(a: unknown, b: unknown) { 2 | if (typeof a !== typeof b) { 3 | return false; 4 | } 5 | if (typeof a === "object" && a !== null) { 6 | if (a === b) { 7 | return true; 8 | } 9 | if (Array.isArray(a)) { 10 | if (a.length !== (b as unknown[]).length) { 11 | return false; 12 | } 13 | for (let index = 0; index < a.length; index++) { 14 | const elementA = a[index]; 15 | const elementB = (b as unknown[])[index]; 16 | const isEqual = deepEqual(elementA, elementB); 17 | if (!isEqual) { 18 | return false; 19 | } 20 | } 21 | return true; 22 | } 23 | const keys = Object.keys(a); 24 | if (keys.length !== Object.keys(b as object).length) { 25 | return false; 26 | } 27 | for (const key of keys) { 28 | const isEqual = deepEqual( 29 | a[key as keyof typeof a], 30 | (b as object)[key as keyof typeof b], 31 | ); 32 | if (!isEqual) { 33 | return false; 34 | } 35 | } 36 | return true; 37 | } 38 | return a === b; 39 | } 40 | -------------------------------------------------------------------------------- /packages/react/src/data-connect/validateReactArgs.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ConnectorConfig, 3 | type DataConnect, 4 | getDataConnect, 5 | } from "firebase/data-connect"; 6 | import type { useDataConnectQueryOptions } from "./useDataConnectQuery"; 7 | 8 | type DataConnectOptions = 9 | | useDataConnectQueryOptions 10 | | useDataConnectQueryOptions; 11 | 12 | interface ParsedReactArgs { 13 | dc: DataConnect; 14 | vars: Variables; 15 | options: DataConnectOptions; 16 | } 17 | 18 | /** 19 | * The generated React SDK will allow the user to pass in variables, a Data Connect instance, or operation options. 20 | * The only required argument is the variables, which are only required when the operation has at least one required 21 | * variable. Otherwise, all arguments are optional. This function validates the variables and returns back the DataConnect 22 | * instance, variables, and options based on the arguments passed in. 23 | * @param connectorConfig DataConnect connector config 24 | * @param dcOrVarsOrOptions the first argument provided to a generated react function 25 | * @param varsOrOptions the second argument provided to a generated react function 26 | * @param options the third argument provided to a generated react function 27 | * @param hasVars boolean parameter indicating whether the operation has variables 28 | * @param validateVars boolean parameter indicating whether we should expect to find a value for realVars 29 | * @returns parsed DataConnect, Variables, and Options for the operation 30 | * @internal 31 | */ 32 | export function validateReactArgs( 33 | connectorConfig: ConnectorConfig, 34 | dcOrVarsOrOptions?: DataConnect | Variables | DataConnectOptions, 35 | varsOrOptions?: Variables | DataConnectOptions, 36 | options?: DataConnectOptions, 37 | hasVars?: boolean, 38 | validateVars?: boolean, 39 | ): ParsedReactArgs { 40 | let dcInstance: DataConnect; 41 | let realVars: Variables; 42 | let realOptions: DataConnectOptions; 43 | 44 | if (dcOrVarsOrOptions && "enableEmulator" in dcOrVarsOrOptions) { 45 | dcInstance = dcOrVarsOrOptions as DataConnect; 46 | if (hasVars) { 47 | realVars = varsOrOptions as Variables; 48 | realOptions = options as DataConnectOptions; 49 | } else { 50 | realVars = undefined as unknown as Variables; 51 | realOptions = varsOrOptions as DataConnectOptions; 52 | } 53 | } else { 54 | dcInstance = getDataConnect(connectorConfig); 55 | if (hasVars) { 56 | realVars = dcOrVarsOrOptions as Variables; 57 | realOptions = varsOrOptions as DataConnectOptions; 58 | } else { 59 | realVars = undefined as unknown as Variables; 60 | realOptions = dcOrVarsOrOptions as DataConnectOptions; 61 | } 62 | } 63 | 64 | if (!dcInstance || (!realVars && validateVars)) { 65 | throw new Error("invalid-argument: Variables required."); // copied from firebase error codes 66 | } 67 | return { dc: dcInstance, vars: realVars, options: realOptions }; 68 | } 69 | -------------------------------------------------------------------------------- /packages/react/src/database/index.ts: -------------------------------------------------------------------------------- 1 | // useRefQuery 2 | // useRefSetMutation 3 | // useRefSetPriorityMutation 4 | // useRefSetWithPriorityMutation 5 | // useRefUpdateMutation 6 | -------------------------------------------------------------------------------- /packages/react/src/firestore/index.ts: -------------------------------------------------------------------------------- 1 | export { useAddDocumentMutation } from "./useAddDocumentMutation"; 2 | export { useClearIndexedDbPersistenceMutation } from "./useClearIndexedDbPersistenceMutation"; 3 | export { useCollectionQuery } from "./useCollectionQuery"; 4 | export { useDeleteDocumentMutation } from "./useDeleteDocumentMutation"; 5 | export { useDisableNetworkMutation } from "./useDisableNetworkMutation"; 6 | export { useDocumentQuery } from "./useDocumentQuery"; 7 | export { useEnableNetworkMutation } from "./useEnableNetworkMutation"; 8 | export { useGetAggregateFromServerQuery } from "./useGetAggregateFromServerQuery"; 9 | export { useGetCountFromServerQuery } from "./useGetCountFromServerQuery"; 10 | export { useNamedQuery } from "./useNamedQuery"; 11 | export { useRunTransactionMutation } from "./useRunTransactionMutation"; 12 | export { useSetDocumentMutation } from "./useSetDocumentMutation"; 13 | export { useUpdateDocumentMutation } from "./useUpdateDocumentMutation"; 14 | export { useWaitForPendingWritesQuery } from "./useWaitForPendingWritesQuery"; 15 | export { useWriteBatchCommitMutation } from "./useWriteBatchCommitMutation"; 16 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useAddDocumentMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | addDoc, 4 | type CollectionReference, 5 | type DocumentData, 6 | type DocumentReference, 7 | type FirestoreError, 8 | type WithFieldValue, 9 | } from "firebase/firestore"; 10 | 11 | type FirestoreUseMutationOptions< 12 | TData = unknown, 13 | TError = Error, 14 | AppModelType extends DocumentData = DocumentData, 15 | > = Omit< 16 | UseMutationOptions>, 17 | "mutationFn" 18 | >; 19 | 20 | export function useAddDocumentMutation< 21 | AppModelType extends DocumentData = DocumentData, 22 | DbModelType extends DocumentData = DocumentData, 23 | >( 24 | collectionRef: CollectionReference, 25 | options?: FirestoreUseMutationOptions< 26 | DocumentReference, 27 | FirestoreError, 28 | AppModelType 29 | >, 30 | ) { 31 | return useMutation< 32 | DocumentReference, 33 | FirestoreError, 34 | WithFieldValue 35 | >({ 36 | ...options, 37 | mutationFn: (data) => addDoc(collectionRef, data), 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useClearIndexedDbPersistenceMutation.test.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 | import { act, renderHook, waitFor } from "@testing-library/react"; 3 | import type React from "react"; 4 | import { beforeEach, describe, expect, test, vi } from "vitest"; 5 | import { firestore, wipeFirestore } from "~/testing-utils"; 6 | import { useClearIndexedDbPersistenceMutation } from "./useClearIndexedDbPersistenceMutation"; 7 | 8 | const queryClient = new QueryClient({ 9 | defaultOptions: { 10 | queries: { retry: false }, 11 | mutations: { retry: false }, 12 | }, 13 | }); 14 | 15 | const wrapper = ({ children }: { children: React.ReactNode }) => ( 16 | {children} 17 | ); 18 | 19 | describe("useClearIndexedDbPersistenceMutation", () => { 20 | beforeEach(async () => { 21 | queryClient.clear(); 22 | await wipeFirestore(); 23 | }); 24 | 25 | test("should successfully clear IndexedDB persistence", async () => { 26 | const { result } = renderHook( 27 | () => useClearIndexedDbPersistenceMutation(firestore), 28 | { 29 | wrapper, 30 | }, 31 | ); 32 | 33 | await act(() => result.current.mutate()); 34 | 35 | await waitFor(() => expect(result.current.isSuccess).toBe(true)); 36 | 37 | expect(result.current.data).toBeUndefined(); 38 | expect(result.current.error).toBeNull(); 39 | expect(result.current.isPending).toBe(false); 40 | }); 41 | 42 | test("should respect custom options passed to the hook", async () => { 43 | const onSuccessMock = vi.fn(); 44 | const onErrorMock = vi.fn(); 45 | 46 | const { result } = renderHook( 47 | () => 48 | useClearIndexedDbPersistenceMutation(firestore, { 49 | onSuccess: onSuccessMock, 50 | onError: onErrorMock, 51 | }), 52 | { wrapper }, 53 | ); 54 | 55 | await act(() => result.current.mutate()); 56 | 57 | await waitFor(() => expect(result.current.isSuccess).toBe(true)); 58 | expect(onSuccessMock).toHaveBeenCalled(); 59 | expect(onErrorMock).not.toHaveBeenCalled(); 60 | }); 61 | 62 | test("should correctly reset mutation state after operations", async () => { 63 | const { result } = renderHook( 64 | () => useClearIndexedDbPersistenceMutation(firestore), 65 | { 66 | wrapper, 67 | }, 68 | ); 69 | 70 | await act(() => result.current.mutate()); 71 | 72 | await waitFor(() => expect(result.current.isSuccess).toBe(true)); 73 | 74 | act(() => result.current.reset()); 75 | 76 | await waitFor(() => { 77 | expect(result.current.isIdle).toBe(true); 78 | expect(result.current.data).toBeUndefined(); 79 | expect(result.current.error).toBeNull(); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useClearIndexedDbPersistenceMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | clearIndexedDbPersistence, 4 | type Firestore, 5 | type FirestoreError, 6 | } from "firebase/firestore"; 7 | 8 | type UseFirestoreMutationOptions = Omit< 9 | UseMutationOptions, 10 | "mutationFn" 11 | >; 12 | 13 | export function useClearIndexedDbPersistenceMutation( 14 | firestore: Firestore, 15 | options?: UseFirestoreMutationOptions, 16 | ) { 17 | return useMutation({ 18 | ...options, 19 | mutationFn: () => clearIndexedDbPersistence(firestore), 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useCollectionQuery.ts: -------------------------------------------------------------------------------- 1 | import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; 2 | import { 3 | type DocumentData, 4 | type FirestoreError, 5 | getDocs, 6 | getDocsFromCache, 7 | getDocsFromServer, 8 | type Query, 9 | type QuerySnapshot, 10 | } from "firebase/firestore"; 11 | 12 | type FirestoreUseQueryOptions = Omit< 13 | UseQueryOptions, 14 | "queryFn" 15 | > & { 16 | firestore?: { 17 | source?: "server" | "cache"; 18 | }; 19 | }; 20 | 21 | export function useCollectionQuery< 22 | FromFirestore extends DocumentData = DocumentData, 23 | ToFirestore extends DocumentData = DocumentData, 24 | >( 25 | query: Query, 26 | options: FirestoreUseQueryOptions< 27 | QuerySnapshot, 28 | FirestoreError 29 | >, 30 | ) { 31 | const { firestore, ...queryOptions } = options; 32 | 33 | return useQuery, FirestoreError>({ 34 | ...queryOptions, 35 | queryFn: async () => { 36 | if (firestore?.source === "server") { 37 | return await getDocsFromServer(query); 38 | } 39 | 40 | if (firestore?.source === "cache") { 41 | return await getDocsFromCache(query); 42 | } 43 | 44 | return await getDocs(query); 45 | }, 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useDeleteDocumentMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | type DocumentData, 4 | type DocumentReference, 5 | deleteDoc, 6 | type FirestoreError, 7 | } from "firebase/firestore"; 8 | 9 | type FirestoreUseMutationOptions = Omit< 10 | UseMutationOptions, 11 | "mutationFn" 12 | >; 13 | 14 | export function useDeleteDocumentMutation< 15 | AppModelType extends DocumentData = DocumentData, 16 | DbModelType extends DocumentData = DocumentData, 17 | >( 18 | documentRef: DocumentReference, 19 | options?: FirestoreUseMutationOptions, 20 | ) { 21 | return useMutation({ 22 | ...options, 23 | mutationFn: () => deleteDoc(documentRef), 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useDisableNetworkMutation.test.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 | import { act, renderHook, waitFor } from "@testing-library/react"; 3 | import { doc, enableNetwork, getDocFromServer } from "firebase/firestore"; 4 | import type React from "react"; 5 | import { beforeEach, describe, expect, test } from "vitest"; 6 | import { 7 | expectFirestoreError, 8 | firestore, 9 | wipeFirestore, 10 | } from "~/testing-utils"; 11 | import { useDisableNetworkMutation } from "./useDisableNetworkMutation"; 12 | 13 | const queryClient = new QueryClient({ 14 | defaultOptions: { 15 | queries: { retry: false }, 16 | mutations: { retry: false }, 17 | }, 18 | }); 19 | 20 | const wrapper = ({ children }: { children: React.ReactNode }) => ( 21 | {children} 22 | ); 23 | 24 | describe("useDisableNetworkMutation", () => { 25 | beforeEach(async () => { 26 | queryClient.clear(); 27 | await enableNetwork(firestore); 28 | await wipeFirestore(); 29 | }); 30 | 31 | test("should successfully disable the Firestore network", async () => { 32 | const { result } = renderHook(() => useDisableNetworkMutation(firestore), { 33 | wrapper, 34 | }); 35 | 36 | await act(() => result.current.mutate()); 37 | 38 | await waitFor(() => expect(result.current.isSuccess).toBe(true)); 39 | 40 | // Verify that network operations fail 41 | const docRef = doc(firestore, "tests", "someDoc"); 42 | 43 | try { 44 | await getDocFromServer(docRef); 45 | throw new Error( 46 | "Expected the network to be disabled, but Firestore operation succeeded.", 47 | ); 48 | } catch (error) { 49 | expectFirestoreError(error, "unavailable"); 50 | } 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useDisableNetworkMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | disableNetwork, 4 | type Firestore, 5 | type FirestoreError, 6 | } from "firebase/firestore"; 7 | 8 | type FirestoreUseMutationOptions = Omit< 9 | UseMutationOptions, 10 | "mutationFn" 11 | >; 12 | 13 | export function useDisableNetworkMutation( 14 | firestore: Firestore, 15 | options?: FirestoreUseMutationOptions, 16 | ) { 17 | return useMutation({ 18 | ...options, 19 | mutationFn: () => disableNetwork(firestore), 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useDocumentQuery.ts: -------------------------------------------------------------------------------- 1 | import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; 2 | import { 3 | type DocumentData, 4 | type DocumentReference, 5 | type DocumentSnapshot, 6 | type FirestoreError, 7 | getDoc, 8 | getDocFromCache, 9 | getDocFromServer, 10 | } from "firebase/firestore"; 11 | 12 | type FirestoreUseQueryOptions = Omit< 13 | UseQueryOptions, 14 | "queryFn" 15 | > & { 16 | firestore?: { 17 | source?: "server" | "cache"; 18 | }; 19 | }; 20 | 21 | export function useDocumentQuery< 22 | FromFirestore extends DocumentData = DocumentData, 23 | ToFirestore extends DocumentData = DocumentData, 24 | >( 25 | documentRef: DocumentReference, 26 | options: FirestoreUseQueryOptions< 27 | DocumentSnapshot, 28 | FirestoreError 29 | >, 30 | ) { 31 | const { firestore, ...queryOptions } = options; 32 | 33 | return useQuery, FirestoreError>( 34 | { 35 | ...queryOptions, 36 | queryFn: async () => { 37 | if (firestore?.source === "server") { 38 | return await getDocFromServer(documentRef); 39 | } 40 | 41 | if (firestore?.source === "cache") { 42 | return await getDocFromCache(documentRef); 43 | } 44 | 45 | return await getDoc(documentRef); 46 | }, 47 | }, 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useEnableNetworkMutation.test.tsx: -------------------------------------------------------------------------------- 1 | import { act, renderHook, waitFor } from "@testing-library/react"; 2 | import { 3 | disableNetwork, 4 | doc, 5 | getDocFromServer, 6 | setDoc, 7 | } from "firebase/firestore"; 8 | import { beforeEach, describe, expect, test, vi } from "vitest"; 9 | import { firestore, wipeFirestore } from "~/testing-utils"; 10 | import { queryClient, wrapper } from "../../utils"; 11 | import { useEnableNetworkMutation } from "./useEnableNetworkMutation"; 12 | 13 | describe("useEnableNetworkMutation", () => { 14 | beforeEach(async () => { 15 | queryClient.clear(); 16 | await disableNetwork(firestore); 17 | await wipeFirestore(); 18 | }); 19 | 20 | test("should successfully enable the Firestore network", async () => { 21 | const docRef = doc(firestore, "tests", "useEnableNetworkMutation"); 22 | const mockData = { library: "tanstack-query-firebase" }; 23 | 24 | const { result } = renderHook(() => useEnableNetworkMutation(firestore), { 25 | wrapper, 26 | }); 27 | 28 | // Enable the network 29 | await act(() => result.current.mutate()); 30 | await waitFor(() => expect(result.current.isSuccess).toBe(true)); 31 | 32 | await setDoc(docRef, mockData); 33 | 34 | const fetchedDoc = await getDocFromServer(docRef); 35 | 36 | expect(fetchedDoc.exists()).toBe(true); 37 | expect(fetchedDoc.data()).toEqual(mockData); 38 | }); 39 | 40 | test("handles mutation options correctly", async () => { 41 | const onSuccessMock = vi.fn(); 42 | const onErrorMock = vi.fn(); 43 | 44 | const { result } = renderHook( 45 | () => 46 | useEnableNetworkMutation(firestore, { 47 | onSuccess: onSuccessMock, 48 | onError: onErrorMock, 49 | }), 50 | { wrapper }, 51 | ); 52 | 53 | await act(() => result.current.mutate()); 54 | 55 | await waitFor(() => { 56 | expect(result.current.isSuccess).toBe(true); 57 | expect(onSuccessMock).toHaveBeenCalled(); 58 | expect(onErrorMock).not.toHaveBeenCalled(); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useEnableNetworkMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | enableNetwork, 4 | type Firestore, 5 | type FirestoreError, 6 | } from "firebase/firestore"; 7 | 8 | type FirestoreUseMutationOptions = Omit< 9 | UseMutationOptions, 10 | "mutationFn" 11 | >; 12 | 13 | export function useEnableNetworkMutation( 14 | firestore: Firestore, 15 | options?: FirestoreUseMutationOptions, 16 | ) { 17 | return useMutation({ 18 | ...options, 19 | mutationFn: () => enableNetwork(firestore), 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useGetAggregateFromServerQuery.ts: -------------------------------------------------------------------------------- 1 | import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; 2 | import { 3 | type AggregateQuerySnapshot, 4 | type AggregateSpec, 5 | type DocumentData, 6 | type FirestoreError, 7 | getAggregateFromServer, 8 | type Query, 9 | } from "firebase/firestore"; 10 | 11 | type FirestoreUseQueryOptions = Omit< 12 | UseQueryOptions, 13 | "queryFn" 14 | >; 15 | 16 | export function useGetAggregateFromServerQuery< 17 | T extends AggregateSpec, 18 | AppModelType = DocumentData, 19 | DbModelType extends DocumentData = DocumentData, 20 | >( 21 | query: Query, 22 | aggregateSpec: T, 23 | options: FirestoreUseQueryOptions< 24 | AggregateQuerySnapshot, 25 | FirestoreError 26 | >, 27 | ) { 28 | return useQuery< 29 | AggregateQuerySnapshot, 30 | FirestoreError 31 | >({ 32 | ...options, 33 | queryFn: () => getAggregateFromServer(query, aggregateSpec), 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useGetCountFromServerQuery.ts: -------------------------------------------------------------------------------- 1 | import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; 2 | import { 3 | type AggregateField, 4 | type AggregateQuerySnapshot, 5 | type DocumentData, 6 | type FirestoreError, 7 | getCountFromServer, 8 | type Query, 9 | } from "firebase/firestore"; 10 | 11 | type FirestoreUseQueryOptions = Omit< 12 | UseQueryOptions, 13 | "queryFn" 14 | >; 15 | 16 | export function useGetCountFromServerQuery< 17 | AppModelType = DocumentData, 18 | DbModelType extends DocumentData = DocumentData, 19 | >( 20 | query: Query, 21 | options: FirestoreUseQueryOptions< 22 | AggregateQuerySnapshot< 23 | { count: AggregateField }, 24 | AppModelType, 25 | DbModelType 26 | >, 27 | FirestoreError 28 | >, 29 | ) { 30 | return useQuery< 31 | AggregateQuerySnapshot< 32 | { count: AggregateField }, 33 | AppModelType, 34 | DbModelType 35 | >, 36 | FirestoreError 37 | >({ 38 | ...options, 39 | queryFn: () => getCountFromServer(query), 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useNamedQuery.ts: -------------------------------------------------------------------------------- 1 | import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; 2 | import { 3 | type DocumentData, 4 | type Firestore, 5 | type FirestoreError, 6 | namedQuery, 7 | type Query, 8 | } from "firebase/firestore"; 9 | 10 | type FirestoreUseQueryOptions = Omit< 11 | UseQueryOptions, 12 | "queryFn" 13 | >; 14 | 15 | export function useNamedQuery< 16 | _AppModelType = DocumentData, 17 | _DbModelType extends DocumentData = DocumentData, 18 | >( 19 | firestore: Firestore, 20 | name: string, 21 | options: FirestoreUseQueryOptions, 22 | ) { 23 | return useQuery({ 24 | ...options, 25 | queryFn: () => namedQuery(firestore, name), 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useRunTransactionMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | type Firestore, 4 | type FirestoreError, 5 | runTransaction, 6 | type Transaction, 7 | type TransactionOptions, 8 | } from "firebase/firestore"; 9 | 10 | type RunTransactionFunction = (transaction: Transaction) => Promise; 11 | 12 | type FirestoreUseMutationOptions = Omit< 13 | UseMutationOptions, 14 | "mutationFn" 15 | > & { 16 | firestore?: TransactionOptions; 17 | }; 18 | 19 | export function useRunTransactionMutation( 20 | firestore: Firestore, 21 | updateFunction: RunTransactionFunction, 22 | options?: FirestoreUseMutationOptions, 23 | ) { 24 | const { firestore: firestoreOptions, ...queryOptions } = options ?? {}; 25 | 26 | return useMutation({ 27 | ...queryOptions, 28 | mutationFn: () => 29 | runTransaction(firestore, updateFunction, firestoreOptions), 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useSetDocumentMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | type DocumentData, 4 | type DocumentReference, 5 | type FirestoreError, 6 | setDoc, 7 | type WithFieldValue, 8 | } from "firebase/firestore"; 9 | 10 | type FirestoreUseMutationOptions< 11 | TData = unknown, 12 | TError = Error, 13 | AppModelType extends DocumentData = DocumentData, 14 | > = Omit< 15 | UseMutationOptions>, 16 | "mutationFn" 17 | >; 18 | 19 | export function useSetDocumentMutation< 20 | AppModelType extends DocumentData = DocumentData, 21 | DbModelType extends DocumentData = DocumentData, 22 | >( 23 | documentRef: DocumentReference, 24 | options?: FirestoreUseMutationOptions, 25 | ) { 26 | return useMutation>({ 27 | ...options, 28 | mutationFn: (data) => setDoc(documentRef, data), 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useUpdateDocumentMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import { 3 | type DocumentData, 4 | type DocumentReference, 5 | type FirestoreError, 6 | type UpdateData, 7 | updateDoc, 8 | } from "firebase/firestore"; 9 | 10 | type FirestoreUseMutationOptions< 11 | TData = unknown, 12 | TError = Error, 13 | DbModelType extends DocumentData = DocumentData, 14 | > = Omit< 15 | UseMutationOptions>, 16 | "mutationFn" 17 | >; 18 | 19 | export function useUpdateDocumentMutation< 20 | AppModelType extends DocumentData = DocumentData, 21 | DbModelType extends DocumentData = DocumentData, 22 | >( 23 | documentRef: DocumentReference, 24 | options?: FirestoreUseMutationOptions, 25 | ) { 26 | return useMutation>({ 27 | ...options, 28 | mutationFn: (data) => updateDoc(documentRef, data), 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useWaitForPendingWritesQuery.test.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 | import { renderHook } from "@testing-library/react"; 3 | import { doc, setDoc } from "firebase/firestore"; 4 | import type React from "react"; 5 | import { beforeEach, describe, expect, test } from "vitest"; 6 | import { firestore, wipeFirestore } from "~/testing-utils"; 7 | import { useWaitForPendingWritesQuery } from "./useWaitForPendingWritesQuery"; 8 | 9 | const queryClient = new QueryClient({ 10 | defaultOptions: { 11 | queries: { retry: false }, 12 | mutations: { retry: false }, 13 | }, 14 | }); 15 | 16 | const wrapper = ({ children }: { children: React.ReactNode }) => ( 17 | {children} 18 | ); 19 | 20 | describe("useWaitForPendingWritesQuery", () => { 21 | beforeEach(async () => { 22 | queryClient.clear(); 23 | await wipeFirestore(); 24 | }); 25 | 26 | test("enters loading state when pending writes are in progress", async () => { 27 | const docRef = doc(firestore, "tests", "loadingStateDoc"); 28 | 29 | const { result } = renderHook( 30 | () => 31 | useWaitForPendingWritesQuery(firestore, { 32 | queryKey: ["pending", "write", "loading"], 33 | }), 34 | { wrapper }, 35 | ); 36 | 37 | // Initiate a write without an await 38 | setDoc(docRef, { value: "loading-test" }); 39 | 40 | expect(result.current.isPending).toBe(true); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useWaitForPendingWritesQuery.ts: -------------------------------------------------------------------------------- 1 | import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; 2 | import { 3 | type Firestore, 4 | type FirestoreError, 5 | waitForPendingWrites, 6 | } from "firebase/firestore"; 7 | 8 | type FirestoreUseQueryOptions = Omit< 9 | UseQueryOptions, 10 | "queryFn" 11 | >; 12 | 13 | export function useWaitForPendingWritesQuery( 14 | firestore: Firestore, 15 | options: FirestoreUseQueryOptions, 16 | ) { 17 | return useQuery({ 18 | ...options, 19 | queryFn: () => waitForPendingWrites(firestore), 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /packages/react/src/firestore/useWriteBatchCommitMutation.ts: -------------------------------------------------------------------------------- 1 | import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; 2 | import type { FirestoreError, WriteBatch } from "firebase/firestore"; 3 | 4 | type FirestoreUseMutationOptions = Omit< 5 | UseMutationOptions, 6 | "mutationFn" 7 | >; 8 | 9 | export function useWriteBatchCommitMutation( 10 | options?: FirestoreUseMutationOptions, 11 | ) { 12 | return useMutation({ 13 | ...options, 14 | mutationFn: (batch: WriteBatch) => batch.commit(), 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /packages/react/src/functions/index.ts: -------------------------------------------------------------------------------- 1 | // useHttpsCallableQuery 2 | // useHttpsCallableMutation 3 | // useHttpsCallableFromURLQuery 4 | // useHttpsCallableFromURLMutation 5 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /packages/react/src/installations/index.ts: -------------------------------------------------------------------------------- 1 | // useDeleteInstallationsMutation 2 | // useGetInstallationsIdQuery 3 | // useGetInstallationsIdMutation 4 | // useGetInstallationsTokenQuery 5 | 6 | // ^^ we add Installations to the name here, is this consistent with the rest of the API? 7 | -------------------------------------------------------------------------------- /packages/react/src/messaging/index.ts: -------------------------------------------------------------------------------- 1 | // useDeleteTokenMutation 2 | // useGetTokenQuery 3 | -------------------------------------------------------------------------------- /packages/react/src/remote-config/index.ts: -------------------------------------------------------------------------------- 1 | // useActivateQuery 2 | // useActivateMutation 3 | // useEnsureInitializedQuery 4 | // useEnsureInitializedMutation 5 | // useFetchAndActivateQuery 6 | // useFetchAndActivateMutation 7 | // useFetchConfigQuery 8 | // useFetchConfigMutation 9 | // useGetAllQuery 10 | -------------------------------------------------------------------------------- /packages/react/src/storage/index.ts: -------------------------------------------------------------------------------- 1 | // useDeleteObjectMutation 2 | // useGetBlobQuery 3 | // useGetBytesQuery 4 | // useGetDownloadURLQuery 5 | // useGetMetadataQuery 6 | // useListQuery 7 | // useListAllQuery 8 | // useUpdateMetadataMutation 9 | // useUploadBytesMutation 10 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "jsx": "react-jsx", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "types": ["vitest/globals"], 13 | "paths": { 14 | "~/testing-utils": ["./vitest/utils.ts"], 15 | "@/dataconnect/*": ["../../dataconnect-sdk/js/*"] 16 | } 17 | }, 18 | "include": ["src", "utils.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/react/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import { defineConfig } from "tsup"; 3 | 4 | const supportedPackages = ["data-connect", "firestore", "auth"]; 5 | export default defineConfig({ 6 | entry: [`src/(${supportedPackages.join("|")})/index.ts`, "src/index.ts"], 7 | format: ["esm"], 8 | dts: true, // generates .d.ts files 9 | outDir: "dist", 10 | external: ["react"], 11 | esbuildOptions(options, _context) { 12 | options.outbase = "./src"; 13 | }, 14 | // splitting: false, // Disable code splitting to generate distinct files 15 | clean: true, 16 | async onSuccess() { 17 | try { 18 | await fs.copyFile("./package.json", "./dist/package.json"); 19 | await fs.copyFile("./README.md", "./dist/README.md"); 20 | await fs.copyFile("./LICENSE", "./dist/LICENSE"); 21 | } catch (e) { 22 | console.error(`Error copying files: ${e}`); 23 | } 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /packages/react/utils.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | QueryClient, 3 | QueryClientProvider, 4 | type UseMutationResult, 5 | } from "@tanstack/react-query"; 6 | import type { ReactNode } from "react"; 7 | import { expect } from "vitest"; 8 | 9 | const queryClient = new QueryClient({ 10 | defaultOptions: { 11 | queries: { 12 | retry: false, 13 | }, 14 | mutations: { 15 | retry: false, 16 | }, 17 | }, 18 | }); 19 | 20 | const wrapper = ({ children }: { children: ReactNode }) => ( 21 | {children} 22 | ); 23 | 24 | export { wrapper, queryClient }; 25 | 26 | // Helper type to make some properties of a type optional. 27 | export type PartialBy = Omit & Partial>; 28 | 29 | export function expectInitialMutationState(result: { 30 | current: UseMutationResult; 31 | }) { 32 | expect(result.current.isSuccess).toBe(false); 33 | expect(result.current.isPending).toBe(false); 34 | expect(result.current.isError).toBe(false); 35 | expect(result.current.isIdle).toBe(true); 36 | expect(result.current.failureCount).toBe(0); 37 | expect(result.current.failureReason).toBeNull(); 38 | expect(result.current.data).toBeUndefined(); 39 | expect(result.current.error).toBeNull(); 40 | } 41 | -------------------------------------------------------------------------------- /packages/react/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | fileParallelism: false, 7 | environment: "happy-dom", 8 | coverage: { 9 | provider: "istanbul", 10 | }, 11 | alias: { 12 | "~/testing-utils": path.resolve(__dirname, "./vitest/utils"), 13 | "@/dataconnect/default-connector": path.resolve( 14 | __dirname, 15 | "../../dataconnect-sdk/js/default-connector", 16 | ), 17 | }, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /packages/react/vitest/utils.ts: -------------------------------------------------------------------------------- 1 | import { type FirebaseApp, FirebaseError, initializeApp } from "firebase/app"; 2 | import { type Auth, connectAuthEmulator, getAuth } from "firebase/auth"; 3 | import { 4 | connectDataConnectEmulator, 5 | getDataConnect, 6 | } from "firebase/data-connect"; 7 | import { 8 | connectFirestoreEmulator, 9 | type Firestore, 10 | getFirestore, 11 | } from "firebase/firestore"; 12 | import { expect } from "vitest"; 13 | import { connectorConfig } from "@/dataconnect/default-connector"; 14 | 15 | const firebaseTestingOptions = { 16 | projectId: "test-project", 17 | apiKey: "test-api-key", 18 | authDomain: "test-auth-domain", 19 | }; 20 | 21 | let firebaseApp: FirebaseApp | undefined; 22 | let firestore: Firestore; 23 | let auth: Auth; 24 | 25 | if (!firebaseApp) { 26 | firebaseApp = initializeApp(firebaseTestingOptions); 27 | firestore = getFirestore(firebaseApp); 28 | auth = getAuth(firebaseApp); 29 | 30 | connectFirestoreEmulator(firestore, "localhost", 8080); 31 | connectAuthEmulator(auth, "http://localhost:9099"); 32 | connectDataConnectEmulator( 33 | getDataConnect(connectorConfig), 34 | "localhost", 35 | 9399, 36 | ); 37 | } 38 | 39 | async function wipeFirestore() { 40 | const response = await fetch( 41 | "http://localhost:8080/emulator/v1/projects/test-project/databases/(default)/documents", 42 | { 43 | method: "DELETE", 44 | }, 45 | ); 46 | 47 | if (!response.ok) { 48 | throw new Error("Failed to wipe firestore"); 49 | } 50 | } 51 | 52 | async function wipeAuth() { 53 | const response = await fetch( 54 | "http://localhost:9099/emulator/v1/projects/test-project/accounts", 55 | { 56 | method: "DELETE", 57 | }, 58 | ); 59 | 60 | if (!response.ok) { 61 | throw new Error("Failed to wipe auth"); 62 | } 63 | } 64 | 65 | function expectFirestoreError(error: unknown, expectedCode: string) { 66 | if (error instanceof FirebaseError) { 67 | expect(error).toBeDefined(); 68 | expect(error.code).toBeDefined(); 69 | expect(error.code).toBe(expectedCode); 70 | } else { 71 | throw new Error( 72 | "Expected a Firestore error, but received a different type.", 73 | ); 74 | } 75 | } 76 | 77 | function expectFirebaseError(error: unknown, expectedCode: string) { 78 | if (error instanceof FirebaseError) { 79 | expect(error).toBeDefined(); 80 | expect(error.code).toBeDefined(); 81 | expect(error.code).toBe(expectedCode); 82 | } else { 83 | console.error("Expected a Firebase error, but received a different type.", { 84 | receivedType: typeof error, 85 | errorDetails: 86 | error instanceof Error 87 | ? { message: error.message, stack: error.stack } 88 | : error, 89 | }); 90 | throw new Error( 91 | "Expected a Firebase error, but received a different type.", 92 | ); 93 | } 94 | } 95 | 96 | export { 97 | firestore, 98 | wipeFirestore, 99 | expectFirestoreError, 100 | firebaseTestingOptions, 101 | auth, 102 | wipeAuth, 103 | firebaseApp, 104 | expectFirebaseError, 105 | }; 106 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'examples/*' 3 | - 'examples/react/*' 4 | - 'packages/*' -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**"] 8 | }, 9 | "test": { 10 | "dependsOn": ["^build"], 11 | "outputs": ["coverage/**"], 12 | "inputs": [ 13 | "src/**/*.tsx", 14 | "src/**/*.ts", 15 | "test/**/*.ts", 16 | "test/**/*.tsx", 17 | "vitest.config.ts" 18 | ], 19 | "cache": false, 20 | "persistent": true 21 | }, 22 | "test:ci": { 23 | "dependsOn": ["^build"], 24 | "outputs": ["coverage/**"], 25 | "inputs": [ 26 | "src/**/*.tsx", 27 | "src/**/*.ts", 28 | "test/**/*.ts", 29 | "test/**/*.tsx", 30 | "vitest.config.ts" 31 | ], 32 | "cache": false 33 | }, 34 | "//#format": { 35 | "cache": false 36 | }, 37 | "//#format:fix": { 38 | "cache": false 39 | }, 40 | "dev": { 41 | "cache": false, 42 | "persistent": true 43 | } 44 | } 45 | } 46 | --------------------------------------------------------------------------------