├── .editorconfig ├── .eslintrc.json ├── .github ├── renovate.json └── workflows │ ├── compressed-size.yml │ ├── push.yml │ └── typedoc.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .releaserc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── app-check.md ├── auth.md ├── database.md ├── firestore.md ├── message.md └── storage.md ├── migrations ├── react-firebase-hooks.md ├── v3.md └── v4.md ├── package-lock.json ├── package.json ├── scripts └── create-modules.js ├── src ├── __testfixtures__ │ ├── index.ts │ ├── newPromise.ts │ └── newSymbol.ts ├── app-check │ ├── index.ts │ └── useAppCheckToken.ts ├── auth │ ├── index.ts │ ├── useAuthIdToken.ts │ ├── useAuthIdTokenResult.ts │ ├── useAuthState.spec.ts │ └── useAuthState.ts ├── common │ ├── index.ts │ └── types.ts ├── database │ ├── index.ts │ ├── internal.ts │ ├── useObject.ts │ ├── useObjectOnce.ts │ ├── useObjectValue.ts │ └── useObjectValueOnce.ts ├── firestore │ ├── index.ts │ ├── internal.spec.ts │ ├── internal.ts │ ├── types.ts │ ├── useAggregateFromServer.ts │ ├── useDocument.spec.ts │ ├── useDocument.ts │ ├── useDocumentData.ts │ ├── useDocumentDataOnce.ts │ ├── useDocumentOnce.ts │ ├── useQueries.test-d.ts │ ├── useQueries.ts │ ├── useQueriesData.test-d.ts │ ├── useQueriesData.ts │ ├── useQueriesDataOnce.ts │ ├── useQueriesOnce.ts │ ├── useQuery.ts │ ├── useQueryData.ts │ ├── useQueryDataOnce.ts │ └── useQueryOnce.ts ├── index.ts ├── internal │ ├── useGet.spec.ts │ ├── useGet.ts │ ├── useIsMounted.spec.ts │ ├── useIsMounted.ts │ ├── useListen.spec.ts │ ├── useListen.ts │ ├── useLoadingValue.spec.ts │ ├── useLoadingValue.ts │ ├── useMultiGet.spec.ts │ ├── useMultiGet.ts │ ├── useMultiListen.spec.ts │ ├── useMultiListen.ts │ ├── useMultiLoadingValue.spec.ts │ ├── useMultiLoadingValue.ts │ ├── usePrevious.ts │ ├── useStableValue.spec.ts │ └── useStableValue.ts ├── messaging │ ├── index.ts │ └── useMessagingToken.ts └── storage │ ├── index.ts │ ├── internal.spec.ts │ ├── internal.ts │ ├── useBlob.ts │ ├── useBytes.ts │ ├── useDownloadURL.ts │ ├── useMetadata.ts │ └── useStream.ts ├── tsconfig.json ├── typedoc.json └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | 7 | indent_style = space 8 | indent_size = 4 9 | 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | max_line_length = 125 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "plugin:react/recommended", 8 | "plugin:react-hooks/recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:jsdoc/recommended-typescript", 11 | "prettier" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "rules": { 15 | "react-hooks/exhaustive-deps": "error" 16 | }, 17 | "overrides": [ 18 | { 19 | "files": ["src/**/*.spec.*", "src/__testfixtures__/*", "src/**/internal.ts", "src/internal/*"], 20 | "rules": { 21 | "jsdoc/require-jsdoc": "off", 22 | "jsdoc/require-returns": "off", 23 | "jsdoc/require-param": "off" 24 | } 25 | } 26 | ], 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:js-lib"], 3 | "packageRules": [ 4 | { 5 | "matchDepTypes": ["devDependencies"], 6 | "matchUpdateTypes": ["patch", "minor"], 7 | "groupName": "dev dependencies (non-major)", 8 | "groupSlug": "dev-dependencies" 9 | } 10 | ], 11 | "ignoreDeps": ["firebase"] 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/compressed-size.yml: -------------------------------------------------------------------------------- 1 | name: Compressed Size 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: preactjs/compressed-size-action@v2 12 | with: 13 | pattern: "**/lib/**/*.js" 14 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Build & Release 2 | 3 | on: 4 | - push 5 | 6 | jobs: 7 | build-test: 8 | name: Build & Test 9 | runs-on: ubuntu-22.04 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | - name: Get node version 16 | id: node-version 17 | run: echo ::set-output name=NODE_VERSION::$(cat .nvmrc) 18 | - name: Use node 19 | uses: actions/setup-node@v4.0.4 20 | with: 21 | node-version: ${{ steps.node-version.outputs.NODE_VERSION }} 22 | cache: "npm" 23 | 24 | - name: Install dependencies 25 | run: npm ci 26 | - name: Build 27 | run: npm run build 28 | - name: Lint 29 | run: npm run lint 30 | - name: Test 31 | run: npm test -- --run --coverage 32 | - name: Typecheck 33 | run: npm run typecheck -- --run 34 | 35 | release: 36 | name: Release 37 | runs-on: ubuntu-22.04 38 | needs: build-test 39 | 40 | # https://github.com/semantic-release/semantic-release/blob/master/docs/recipes/ci-configurations/github-actions.md#node-project-configuration 41 | permissions: 42 | contents: write # to be able to publish a GitHub release 43 | issues: write # to be able to comment on released issues 44 | pull-requests: write # to be able to comment on released pull requests 45 | id-token: write # to enable use of OIDC for npm provenance 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | with: 50 | fetch-depth: 0 51 | - name: Get node version 52 | id: node-version 53 | run: echo ::set-output name=NODE_VERSION::$(cat .nvmrc) 54 | - name: Use node 55 | uses: actions/setup-node@v4.0.4 56 | with: 57 | node-version: ${{ steps.node-version.outputs.NODE_VERSION }} 58 | cache: "npm" 59 | 60 | - name: Install dependencies 61 | run: npm ci 62 | - name: Release 63 | run: npm run semantic-release 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 67 | -------------------------------------------------------------------------------- /.github/workflows/typedoc.yml: -------------------------------------------------------------------------------- 1 | name: TypeDoc 2 | 3 | on: 4 | - push 5 | 6 | jobs: 7 | build-deploy: 8 | runs-on: ubuntu-22.04 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Switch node version 13 | shell: bash -l {0} 14 | run: nvm install 15 | - name: Install 16 | run: npm ci 17 | - name: TypeDoc 18 | run: npm run typedoc 19 | - name: Deploy 20 | uses: JamesIves/github-pages-deploy-action@v4.6.3 21 | with: 22 | branch: gh-pages 23 | folder: typedocs 24 | clean: true 25 | dry-run: ${{ github.ref != 'refs/heads/main' }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode/ 2 | /coverage/ 3 | /lib/ 4 | /typedocs/ 5 | /node_modules/ 6 | 7 | # modules 8 | /app-check/ 9 | /auth/ 10 | /database/ 11 | /firestore/ 12 | /messaging/ 13 | /storage/ -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint-staged 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | provenance=true 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.18.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | /lib/ 3 | /typedoc/ 4 | /node_modules/ 5 | /test-results/ 6 | 7 | # modules 8 | /auth/ 9 | /database/ 10 | /firestore/ 11 | /messaging/ 12 | /storage/ 13 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main", 4 | { 5 | "name": "next", 6 | "prerelease": true 7 | }, 8 | { 9 | "name": "next-major", 10 | "prerelease": true 11 | } 12 | ], 13 | "plugins": [ 14 | "@semantic-release/commit-analyzer", 15 | "@semantic-release/release-notes-generator", 16 | "@semantic-release/changelog", 17 | "@semantic-release/npm", 18 | [ 19 | "@semantic-release/git", 20 | { 21 | "message": "chore: ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 22 | } 23 | ], 24 | [ 25 | "@semantic-release/github", 26 | { 27 | "successComment": false 28 | } 29 | ] 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [4.2.0](https://github.com/andipaetzold/react-firehooks/compare/v4.1.1...v4.2.0) (2024-10-23) 2 | 3 | 4 | ### Features 5 | 6 | * support firebase v11 ([7dad34e](https://github.com/andipaetzold/react-firehooks/commit/7dad34ea231e2818d5f5642ef469b86cf810d7cf)) 7 | 8 | ## [4.1.1](https://github.com/andipaetzold/react-firehooks/compare/v4.1.0...v4.1.1) (2024-08-15) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * initial state was reset in single subscription hooks in strict mode in dev ([d0db2af](https://github.com/andipaetzold/react-firehooks/commit/d0db2af76434a617d7e87cf9b0a7ff9f716880f4)) 14 | * race condition in multi "*Once" hooks ([e7af873](https://github.com/andipaetzold/react-firehooks/commit/e7af8731a38d823789f97ecaa7569d20e8d8fdcb)) 15 | * race condition in single `*Once` hooks when reference changes ([d835504](https://github.com/andipaetzold/react-firehooks/commit/d835504c253017bf08bb205994949d2dc38abcc3)) 16 | 17 | # [4.1.0](https://github.com/andipaetzold/react-firehooks/compare/v4.0.3...v4.1.0) (2024-03-14) 18 | 19 | 20 | ### Features 21 | 22 | * **firestore:** support `SnapshotListenOptions.source` ([#267](https://github.com/andipaetzold/react-firehooks/issues/267)) ([aa74cda](https://github.com/andipaetzold/react-firehooks/commit/aa74cda8ee919b4975d2cbff718ecf573b9f5ac6)) 23 | 24 | ## [4.0.3](https://github.com/andipaetzold/react-firehooks/compare/v4.0.2...v4.0.3) (2023-11-25) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * `exactOptionalPropertyTypes` TS option compatibility ([#256](https://github.com/andipaetzold/react-firehooks/issues/256)) ([581774a](https://github.com/andipaetzold/react-firehooks/commit/581774a17f3632607d6bb2b6e0244e14a450a0cc)) 30 | 31 | ## [4.0.2](https://github.com/andipaetzold/react-firehooks/compare/v4.0.1...v4.0.2) (2023-11-05) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * infinite re-renders of `useAggregateFromServer` ([8882819](https://github.com/andipaetzold/react-firehooks/commit/8882819e9a354e07395911d863bb79a48b245cc2)) 37 | 38 | ## [4.0.1](https://github.com/andipaetzold/react-firehooks/compare/v4.0.0...v4.0.1) (2023-11-05) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * typo `useQuerie` -> `useQueries` ([#254](https://github.com/andipaetzold/react-firehooks/issues/254)) ([9a52945](https://github.com/andipaetzold/react-firehooks/commit/9a52945670578a52aa686062d28ab2172d6e125c)) 44 | 45 | # [4.0.0](https://github.com/andipaetzold/react-firehooks/compare/v3.1.0...v4.0.0) (2023-10-26) 46 | 47 | 48 | ### Features 49 | 50 | * remove `useCountFromServer` ([#248](https://github.com/andipaetzold/react-firehooks/issues/248)) ([f2724ed](https://github.com/andipaetzold/react-firehooks/commit/f2724edbcd20fd57a26b8860a3ff59a8d26fc3ae)) 51 | * require firebase 10.5.0 or later ([#247](https://github.com/andipaetzold/react-firehooks/issues/247)) ([03cf52f](https://github.com/andipaetzold/react-firehooks/commit/03cf52ff81cab28b935b3b66b8aba8577a0759be)) 52 | * useAggregateFromServer ([#192](https://github.com/andipaetzold/react-firehooks/issues/192)) ([327a210](https://github.com/andipaetzold/react-firehooks/commit/327a2106711b913351f0e942e27c4be7483fbd13)) 53 | 54 | 55 | ### BREAKING CHANGES 56 | 57 | * `useCountFromServer` was removed in favor of 58 | `useAggregateFromServer` 59 | * require firebase 10.5.0 or later 60 | 61 | # [4.0.0-next.2](https://github.com/andipaetzold/react-firehooks/compare/v4.0.0-next.1...v4.0.0-next.2) (2023-10-12) 62 | 63 | 64 | ### Features 65 | 66 | * remove `useCountFromServer` ([#248](https://github.com/andipaetzold/react-firehooks/issues/248)) ([f2724ed](https://github.com/andipaetzold/react-firehooks/commit/f2724edbcd20fd57a26b8860a3ff59a8d26fc3ae)) 67 | 68 | 69 | ### BREAKING CHANGES 70 | 71 | * `useCountFromServer` was removed in favor of 72 | `useAggregateFromServer` 73 | 74 | # [4.0.0-next.1](https://github.com/andipaetzold/react-firehooks/compare/v3.1.0...v4.0.0-next.1) (2023-10-12) 75 | 76 | 77 | ### Features 78 | 79 | * require firebase 10.5.0 or later ([#247](https://github.com/andipaetzold/react-firehooks/issues/247)) ([03cf52f](https://github.com/andipaetzold/react-firehooks/commit/03cf52ff81cab28b935b3b66b8aba8577a0759be)) 80 | * useAggregateFromServer ([#192](https://github.com/andipaetzold/react-firehooks/issues/192)) ([327a210](https://github.com/andipaetzold/react-firehooks/commit/327a2106711b913351f0e942e27c4be7483fbd13)) 81 | 82 | 83 | ### BREAKING CHANGES 84 | 85 | * require firebase 10.5.0 or later 86 | 87 | # [3.1.0](https://github.com/andipaetzold/react-firehooks/compare/v3.0.0...v3.1.0) (2023-09-19) 88 | 89 | 90 | ### Features 91 | 92 | * refetch/resubscribe when options change ([#240](https://github.com/andipaetzold/react-firehooks/issues/240)) ([f9c30ea](https://github.com/andipaetzold/react-firehooks/commit/f9c30ea98116aa89ef64aae74a000340bece9e6c)) 93 | * useQueriesOnce & useQueriesDataOnce ([#239](https://github.com/andipaetzold/react-firehooks/issues/239)) ([0380229](https://github.com/andipaetzold/react-firehooks/commit/0380229696262f3cec3acf3b6a9b365aca50dc91)) 94 | 95 | # [3.0.0](https://github.com/andipaetzold/react-firehooks/compare/v2.5.0...v3.0.0) (2023-08-01) 96 | 97 | 98 | ### Bug Fixes 99 | 100 | * **firestore:** remove dynamic import from useCountFromServer ([#162](https://github.com/andipaetzold/react-firehooks/issues/162)) ([47a03f6](https://github.com/andipaetzold/react-firehooks/commit/47a03f60baa82768d7fc95b703a14493ca588f3b)) 101 | * properly cleanup `useQueries*` on unmount ([b77becc](https://github.com/andipaetzold/react-firehooks/commit/b77becc381f41c633455f9b7d8d5bc5b0c7db469)) 102 | 103 | 104 | ### Features 105 | 106 | * rename `useCollection*` hooks to `useQuery*` ([#194](https://github.com/andipaetzold/react-firehooks/issues/194)) ([372f055](https://github.com/andipaetzold/react-firehooks/commit/372f055322bf7f273c9736e3bb80969bef2e046f)) 107 | * require firebase 9.11.0 or later ([#148](https://github.com/andipaetzold/react-firehooks/issues/148)) ([7da01b0](https://github.com/andipaetzold/react-firehooks/commit/7da01b003caeaa82fd293549c821fc9c5bc3997e)) 108 | * target es2017 instead of es2015 ([#163](https://github.com/andipaetzold/react-firehooks/issues/163)) ([be187d0](https://github.com/andipaetzold/react-firehooks/commit/be187d03301b156eb032eecb9076b94395bd69e2)) 109 | * useQueries & useQueriesData ([#193](https://github.com/andipaetzold/react-firehooks/issues/193)) ([03be7c8](https://github.com/andipaetzold/react-firehooks/commit/03be7c8054d7e1b2ed4716a6af807a35ab267057)) 110 | 111 | 112 | ### BREAKING CHANGES 113 | 114 | * `useCollection*` hooks are renamed to `useQuery*` 115 | * require firebase 9.11.0 or later 116 | * package targets es2017 117 | 118 | # [3.0.0-next.8](https://github.com/andipaetzold/react-firehooks/compare/v3.0.0-next.7...v3.0.0-next.8) (2023-07-07) 119 | 120 | 121 | ### Features 122 | 123 | * support firebase v10 ([#217](https://github.com/andipaetzold/react-firehooks/issues/217)) ([5119385](https://github.com/andipaetzold/react-firehooks/commit/51193857619ee963d6e40089d8bafef71e699caa)) 124 | 125 | # [2.5.0](https://github.com/andipaetzold/react-firehooks/compare/v2.4.0...v2.5.0) (2023-07-07) 126 | 127 | 128 | ### Features 129 | 130 | * support firebase v10 ([#217](https://github.com/andipaetzold/react-firehooks/issues/217)) ([5119385](https://github.com/andipaetzold/react-firehooks/commit/51193857619ee963d6e40089d8bafef71e699caa)) 131 | 132 | # [3.0.0-next.7](https://github.com/andipaetzold/react-firehooks/compare/v3.0.0-next.6...v3.0.0-next.7) (2023-02-24) 133 | 134 | 135 | ### Bug Fixes 136 | 137 | * rename `useCollection*` hooks to `useQuery*` ([#198](https://github.com/andipaetzold/react-firehooks/issues/198)) ([47e58ef](https://github.com/andipaetzold/react-firehooks/commit/47e58ef51685a0bc38ddae1d348c6d0f2f4a4267)) 138 | 139 | 140 | ### Features 141 | 142 | * useQueries & useQueriesData ([#199](https://github.com/andipaetzold/react-firehooks/issues/199)) ([e3a832d](https://github.com/andipaetzold/react-firehooks/commit/e3a832d1ecae91795954a5cf10663c46949facb5)) 143 | 144 | # [2.4.0](https://github.com/andipaetzold/react-firehooks/compare/v2.3.0...v2.4.0) (2023-02-24) 145 | 146 | 147 | ### Bug Fixes 148 | 149 | * rename `useCollection*` hooks to `useQuery*` ([#198](https://github.com/andipaetzold/react-firehooks/issues/198)) ([47e58ef](https://github.com/andipaetzold/react-firehooks/commit/47e58ef51685a0bc38ddae1d348c6d0f2f4a4267)) 150 | 151 | 152 | ### Features 153 | 154 | * useQueries & useQueriesData ([#199](https://github.com/andipaetzold/react-firehooks/issues/199)) ([e3a832d](https://github.com/andipaetzold/react-firehooks/commit/e3a832d1ecae91795954a5cf10663c46949facb5)) 155 | 156 | # [3.0.0-next.6](https://github.com/andipaetzold/react-firehooks/compare/v3.0.0-next.5...v3.0.0-next.6) (2023-02-11) 157 | 158 | 159 | ### Bug Fixes 160 | 161 | * properly cleanup `useQueries*` on unmount ([b77becc](https://github.com/andipaetzold/react-firehooks/commit/b77becc381f41c633455f9b7d8d5bc5b0c7db469)) 162 | 163 | # [3.0.0-next.5](https://github.com/andipaetzold/react-firehooks/compare/v3.0.0-next.4...v3.0.0-next.5) (2023-02-11) 164 | 165 | 166 | ### Features 167 | 168 | * useQueries & useQueriesData ([#193](https://github.com/andipaetzold/react-firehooks/issues/193)) ([03be7c8](https://github.com/andipaetzold/react-firehooks/commit/03be7c8054d7e1b2ed4716a6af807a35ab267057)) 169 | 170 | # [3.0.0-next.4](https://github.com/andipaetzold/react-firehooks/compare/v3.0.0-next.3...v3.0.0-next.4) (2023-02-10) 171 | 172 | 173 | ### Features 174 | 175 | * rename `useCollection*` hooks to `useQuery*` ([#194](https://github.com/andipaetzold/react-firehooks/issues/194)) ([372f055](https://github.com/andipaetzold/react-firehooks/commit/372f055322bf7f273c9736e3bb80969bef2e046f)) 176 | 177 | 178 | ### BREAKING CHANGES 179 | 180 | * `useCollection*` hooks are renamed to `useQuery*` 181 | 182 | # [3.0.0-next.3](https://github.com/andipaetzold/react-firehooks/compare/v3.0.0-next.2...v3.0.0-next.3) (2022-11-25) 183 | 184 | 185 | ### Bug Fixes 186 | 187 | * **firestore:** remove dynamic import from useCountFromServer ([#162](https://github.com/andipaetzold/react-firehooks/issues/162)) ([47a03f6](https://github.com/andipaetzold/react-firehooks/commit/47a03f60baa82768d7fc95b703a14493ca588f3b)) 188 | 189 | # [3.0.0-next.2](https://github.com/andipaetzold/react-firehooks/compare/v3.0.0-next.1...v3.0.0-next.2) (2022-11-25) 190 | 191 | 192 | ### Features 193 | 194 | * require firebase 9.11.0 or later ([#148](https://github.com/andipaetzold/react-firehooks/issues/148)) ([7da01b0](https://github.com/andipaetzold/react-firehooks/commit/7da01b003caeaa82fd293549c821fc9c5bc3997e)) 195 | 196 | 197 | ### BREAKING CHANGES 198 | 199 | * require firebase 9.11.0 or later 200 | 201 | # [3.0.0-next.1](https://github.com/andipaetzold/react-firehooks/compare/v2.3.0...v3.0.0-next.1) (2022-11-25) 202 | 203 | 204 | ### Features 205 | 206 | * target es2017 instead of es2015 ([#163](https://github.com/andipaetzold/react-firehooks/issues/163)) ([be187d0](https://github.com/andipaetzold/react-firehooks/commit/be187d03301b156eb032eecb9076b94395bd69e2)) 207 | 208 | 209 | ### BREAKING CHANGES 210 | 211 | * package targets es2017 212 | 213 | # [2.3.0](https://github.com/andipaetzold/react-firehooks/compare/v2.2.0...v2.3.0) (2022-10-24) 214 | 215 | 216 | ### Bug Fixes 217 | 218 | * **app-check:** hook was not properly published ([#166](https://github.com/andipaetzold/react-firehooks/issues/166)) ([061acc8](https://github.com/andipaetzold/react-firehooks/commit/061acc8175824862076c596d2fc653a6f1113f24)) 219 | 220 | 221 | ### Features 222 | 223 | * **database/firestore:** add `initialValue` option ([#165](https://github.com/andipaetzold/react-firehooks/issues/165)) ([8c2905c](https://github.com/andipaetzold/react-firehooks/commit/8c2905c8094de9953327143eba84fdf440c2fbc1)) 224 | 225 | # [2.2.0](https://github.com/andipaetzold/react-firehooks/compare/v2.1.0...v2.2.0) (2022-10-18) 226 | 227 | 228 | ### Features 229 | 230 | * **firestore:** add useCountFromServer ([#147](https://github.com/andipaetzold/react-firehooks/issues/147)) ([b04a692](https://github.com/andipaetzold/react-firehooks/commit/b04a692094676b3f99bdbc303d807b71946f067c)) 231 | 232 | # [2.2.0-next.1](https://github.com/andipaetzold/react-firehooks/compare/v2.1.0...v2.2.0-next.1) (2022-10-18) 233 | 234 | 235 | ### Features 236 | 237 | * **firestore:** add useCountFromServer ([#147](https://github.com/andipaetzold/react-firehooks/issues/147)) ([b04a692](https://github.com/andipaetzold/react-firehooks/commit/b04a692094676b3f99bdbc303d807b71946f067c)) 238 | 239 | # [2.1.0](https://github.com/andipaetzold/react-firehooks/compare/v2.0.1...v2.1.0) (2022-10-14) 240 | 241 | 242 | ### Features 243 | 244 | * **app-check:** add useAppCheckToken ([#158](https://github.com/andipaetzold/react-firehooks/issues/158)) ([81d95c2](https://github.com/andipaetzold/react-firehooks/commit/81d95c242c1a4af4103249ceb86ec1fabb4ae550)) 245 | * **auth:** add useAuthIdToken ([#156](https://github.com/andipaetzold/react-firehooks/issues/156)) ([f351734](https://github.com/andipaetzold/react-firehooks/commit/f351734ee510c6e4b6f5aa5d39bc542ed3b2f36a)) 246 | * **auth:** add useAuthIdTokenResult ([#157](https://github.com/andipaetzold/react-firehooks/issues/157)) ([654340a](https://github.com/andipaetzold/react-firehooks/commit/654340a4b4c3107b72480a8fd477eed343584c40)) 247 | 248 | ## [2.0.1](https://github.com/andipaetzold/react-firehooks/compare/v2.0.0...v2.0.1) (2021-12-30) 249 | 250 | 251 | ### Bug Fixes 252 | 253 | * fully specify file paths in imports ([86e1205](https://github.com/andipaetzold/react-firehooks/commit/86e120592bbf89233c23cc416b1f48341962bce2)) 254 | 255 | ## [2.0.1-next.1](https://github.com/andipaetzold/react-firehooks/compare/v2.0.0...v2.0.1-next.1) (2021-12-30) 256 | 257 | 258 | ### Bug Fixes 259 | 260 | * fully specify file paths in imports ([86e1205](https://github.com/andipaetzold/react-firehooks/commit/86e120592bbf89233c23cc416b1f48341962bce2)) 261 | 262 | # [2.0.0](https://github.com/andipaetzold/react-firehooks/compare/v1.7.0...v2.0.0) (2021-12-29) 263 | 264 | 265 | ### Features 266 | 267 | * make package ESM only ([2fff54d](https://github.com/andipaetzold/react-firehooks/commit/2fff54dd26264f18a4afaf5ed6dffa4c4be330d3)) 268 | * require firebase 9.5.0 or later ([8bb9ba1](https://github.com/andipaetzold/react-firehooks/commit/8bb9ba1f5e1fd2c20da49f1ea312d81764593470)) 269 | 270 | 271 | ### BREAKING CHANGES 272 | 273 | * make package ESM only 274 | * require firebase 9.5.0 or later 275 | 276 | # [1.7.0](https://github.com/andipaetzold/react-firehooks/compare/v1.6.0...v1.7.0) (2021-12-29) 277 | 278 | 279 | ### Features 280 | 281 | * **storage:** add useBlob and useStream ([979b798](https://github.com/andipaetzold/react-firehooks/commit/979b7987704bd3731cdd59668b5496adb7c60fa5)) 282 | 283 | # [1.6.0](https://github.com/andipaetzold/react-firehooks/compare/v1.5.1...v1.6.0) (2021-12-21) 284 | 285 | 286 | ### Features 287 | 288 | * **storage:** add useMetadata ([dbb25e0](https://github.com/andipaetzold/react-firehooks/commit/dbb25e042c57e5cdf1dde36b0aea5ca0d7aeca6e)) 289 | 290 | ## [1.5.1](https://github.com/andipaetzold/react-firehooks/compare/v1.5.0...v1.5.1) (2021-12-21) 291 | 292 | 293 | ### Bug Fixes 294 | 295 | * add missing `messaging` folder to package ([0fbfc6d](https://github.com/andipaetzold/react-firehooks/commit/0fbfc6de903ca359f1d249b3eee57272194305b6)) 296 | 297 | # [1.5.0](https://github.com/andipaetzold/react-firehooks/compare/v1.4.2...v1.5.0) (2021-11-19) 298 | 299 | 300 | ### Features 301 | 302 | * **storage:** add `useBytes` ([#31](https://github.com/andipaetzold/react-firehooks/issues/31)) ([462089f](https://github.com/andipaetzold/react-firehooks/commit/462089f10cc98317be785e3c6f104104aa1e2bcf)) 303 | 304 | # [1.5.0-next.2](https://github.com/andipaetzold/react-firehooks/compare/v1.5.0-next.1...v1.5.0-next.2) (2021-11-19) 305 | 306 | 307 | ### Bug Fixes 308 | 309 | * **storage:** use `require` in `useBytes` ([9822fa0](https://github.com/andipaetzold/react-firehooks/commit/9822fa03a4c2f1cf0c7809c049073157a2a20bbc)) 310 | 311 | # [1.5.0-next.1](https://github.com/andipaetzold/react-firehooks/compare/v1.4.2...v1.5.0-next.1) (2021-11-19) 312 | 313 | 314 | ### Features 315 | 316 | * **storage:** add `useBytes` ([205869c](https://github.com/andipaetzold/react-firehooks/commit/205869c5b372e69112280400e3a652e4502a7a41)) 317 | 318 | ## [1.4.2](https://github.com/andipaetzold/react-firehooks/compare/v1.4.1...v1.4.2) (2021-10-22) 319 | 320 | 321 | ### Bug Fixes 322 | 323 | * loading state for undefined refs/queries ([#16](https://github.com/andipaetzold/react-firehooks/issues/16)) ([385a5de](https://github.com/andipaetzold/react-firehooks/commit/385a5def225df12b521b83896495104ea0e9d82f)) 324 | 325 | ## [1.4.1](https://github.com/andipaetzold/react-firehooks/compare/v1.4.0...v1.4.1) (2021-10-20) 326 | 327 | 328 | ### Bug Fixes 329 | 330 | * **auth:** only use currentUser as default if signed in ([3c9086d](https://github.com/andipaetzold/react-firehooks/commit/3c9086dd845bcc488b942d8e129f8ac046fb1c02)) 331 | 332 | # [1.4.0](https://github.com/andipaetzold/react-firehooks/compare/v1.3.1...v1.4.0) (2021-10-17) 333 | 334 | 335 | ### Bug Fixes 336 | 337 | * **auth:** don't return loading state if currentUser is set ([fa53819](https://github.com/andipaetzold/react-firehooks/commit/fa53819683852f0546d158fe037a73b86dc4d53c)) 338 | 339 | 340 | ### Features 341 | 342 | * **messaging:** add useMessagingToken ([#9](https://github.com/andipaetzold/react-firehooks/issues/9)) ([8bc1f4e](https://github.com/andipaetzold/react-firehooks/commit/8bc1f4e5443ea4e0189bf054d017c96dcaabb2d2)) 343 | 344 | ## [1.3.1](https://github.com/andipaetzold/react-firehooks/compare/v1.3.0...v1.3.1) (2021-10-17) 345 | 346 | 347 | ### Bug Fixes 348 | 349 | * **auth:** skip initial load if currentUser is set ([b345137](https://github.com/andipaetzold/react-firehooks/commit/b345137f83bf05e48c9906a887b84b4d744a1315)) 350 | 351 | # [1.3.0](https://github.com/andipaetzold/react-firehooks/compare/v1.2.4...v1.3.0) (2021-10-17) 352 | 353 | 354 | ### Features 355 | 356 | * **database:** add converter option to useObjectValue hooks ([8cb8511](https://github.com/andipaetzold/react-firehooks/commit/8cb8511f446ab31cbe3543be3dcbeddc6ecaf7ad)) 357 | 358 | ## [1.2.4](https://github.com/andipaetzold/react-firehooks/compare/v1.2.3...v1.2.4) (2021-10-15) 359 | 360 | 361 | ### Bug Fixes 362 | 363 | * **database:** correctly name useObject & useObjectOnce ([4c7b83e](https://github.com/andipaetzold/react-firehooks/commit/4c7b83eeb5abe3337663fe65925f1b0372d75347)) 364 | * **database:** export all hooks ([704065c](https://github.com/andipaetzold/react-firehooks/commit/704065c76055580a430ed107c3a52a21a2789004)) 365 | 366 | ## [1.2.3](https://github.com/andipaetzold/react-firehooks/compare/v1.2.2...v1.2.3) (2021-10-15) 367 | 368 | 369 | ### Bug Fixes 370 | 371 | * **database:** use new query when changing from/to undefined ([a096c41](https://github.com/andipaetzold/react-firehooks/commit/a096c41223a45c096172a7e7b60fd4e11938aaa4)) 372 | * **firestore:** use new ref/query when changing from/to undefined ([981ec92](https://github.com/andipaetzold/react-firehooks/commit/981ec922daf76fbfb3618c326de76910d306890b)) 373 | 374 | ## [1.2.2](https://github.com/andipaetzold/react-firehooks/compare/v1.2.1...v1.2.2) (2021-10-15) 375 | 376 | 377 | ### Bug Fixes 378 | 379 | * **firestore:** remove GetOptions ([cd08bd3](https://github.com/andipaetzold/react-firehooks/commit/cd08bd38a53459d8c50aa105ed179ae644539719)) 380 | 381 | ## [1.2.1](https://github.com/andipaetzold/react-firehooks/compare/v1.2.0...v1.2.1) (2021-10-15) 382 | 383 | 384 | ### Bug Fixes 385 | 386 | * mark modules as side-effect-free ([235029e](https://github.com/andipaetzold/react-firehooks/commit/235029ef741070d834f16e846a0793e7939148ce)) 387 | 388 | # [1.2.0](https://github.com/andipaetzold/react-firehooks/compare/v1.1.2...v1.2.0) (2021-10-15) 389 | 390 | 391 | ### Features 392 | 393 | * **auth:** add auth module ([12ab9ca](https://github.com/andipaetzold/react-firehooks/commit/12ab9ca5390e81cea63ac7a93d39202cc36da23f)) 394 | * **database:** add database module ([e202767](https://github.com/andipaetzold/react-firehooks/commit/e20276768cca9fa6e3a23b7a34a7f5a3ee81acca)) 395 | 396 | ## [1.1.2](https://github.com/andipaetzold/react-firehooks/compare/v1.1.1...v1.1.2) (2021-10-14) 397 | 398 | 399 | ### Bug Fixes 400 | 401 | * export individual modules with package.json ([18ab7e3](https://github.com/andipaetzold/react-firehooks/commit/18ab7e3d06100b397d6cdc4e36b1c0a755669bdd)) 402 | 403 | ## [1.1.1](https://github.com/andipaetzold/react-firehooks/compare/v1.1.0...v1.1.1) (2021-10-14) 404 | 405 | 406 | ### Bug Fixes 407 | 408 | * allow null & undefined as storage reference ([3c95bd5](https://github.com/andipaetzold/react-firehooks/commit/3c95bd58c5b0c972dd26e4334981919b83f7682f)) 409 | 410 | # [1.1.0](https://github.com/andipaetzold/react-firehooks/compare/v1.0.1...v1.1.0) (2021-10-14) 411 | 412 | 413 | ### Features 414 | 415 | * add one export per module ([c755b75](https://github.com/andipaetzold/react-firehooks/commit/c755b757b451f21072a2a0eb95106576fe706d5d)) 416 | * add storage module ([32a6d4d](https://github.com/andipaetzold/react-firehooks/commit/32a6d4dc9bf205afbc14cdf6077fc8459dd2d805)) 417 | 418 | ## [1.0.1](https://github.com/andipaetzold/react-firehooks/compare/v1.0.0...v1.0.1) (2021-10-14) 419 | 420 | 421 | ### Bug Fixes 422 | 423 | * accept null as reference & query ([1a4300e](https://github.com/andipaetzold/react-firehooks/commit/1a4300e7bd0727196dd69d311d76fb25f3b43526)) 424 | 425 | # 1.0.0 (2021-10-14) 426 | 427 | 428 | ### Features 429 | 430 | * firestore hooks ([37ac438](https://github.com/andipaetzold/react-firehooks/commit/37ac438a6886d75b732e1d7fca73d1a65a43928c)) 431 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Andi Pätzold 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/v/react-firehooks)](https://www.npmjs.com/package/react-firehooks) 2 | [![downloads](https://img.shields.io/npm/dm/react-firehooks)](https://www.npmjs.com/package/react-firehooks) 3 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-firehooks)](https://bundlephobia.com/package/react-firehooks) 4 | [![tests](https://github.com/andipaetzold/react-firehooks/actions/workflows/push.yml/badge.svg?branch=main)](https://github.com/andipaetzold/react-firehooks/actions/workflows/push.yml?query=branch%3Amain) 5 | [![license](https://img.shields.io/github/license/andipaetzold/react-firehooks)](https://github.com/andipaetzold/react-firehooks/blob/main/LICENSE) 6 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 7 | 8 | # React Firehooks 🔥🪝 9 | 10 | Lightweight dependency-free collection of React hooks for Firebase. 11 | 12 | ## Installation 13 | 14 | ```sh 15 | npm install react-firehooks 16 | ``` 17 | 18 | or 19 | 20 | ```sh 21 | yarn add react-firehooks 22 | ``` 23 | 24 | ## Compatibility 25 | 26 | - [firebase](https://www.npmjs.com/package/firebase): 10.5.0 or later 27 | - [react](https://www.npmjs.com/package/react): 16.8.0 or later 28 | 29 | ## Usage 30 | 31 | [Type Documentation](https://andipaetzold.github.io/react-firehooks) 32 | 33 | This library consists of 6 modules with many hooks: 34 | 35 | - [`app-check`](docs/app-check.md) 36 | - [`auth`](docs/auth.md) 37 | - [`database`](docs/database.md) 38 | - [`firestore`](docs/firestore.md) 39 | - [`messaging`](docs/message.md) 40 | - [`storage`](docs/storage.md) 41 | 42 | All hooks can be imported from `react-firehooks` directly or via `react-firehooks/` to improve tree-shaking and bundle size. 43 | 44 | ## Development 45 | 46 | ### Build 47 | 48 | To build the library, first install the dependencies, then run `npm run build`. 49 | 50 | ```sh 51 | npm install 52 | npm run build 53 | ``` 54 | 55 | ### Tests 56 | 57 | To run the tests, first install the dependencies, then run `npm test`. Watch mode can be started with `npm test -- --watch`. 58 | 59 | ```sh 60 | npm install 61 | npm test 62 | ``` 63 | 64 | ## Resources 65 | 66 | ### React Firebase Hooks 67 | 68 | This library is heavily inspired by [`react-firebase-hooks`](https://www.npmjs.com/package/react-firebase-hooks). It was created because `react-firebase-hooks` seemed unmaintained and did not support Firebase v9 for a couple of months. `react-firehooks` is not a fork but a completely new code base exporting almost identical hooks. 69 | 70 | ## License 71 | 72 | [MIT](LICENSE) 73 | -------------------------------------------------------------------------------- /docs/app-check.md: -------------------------------------------------------------------------------- 1 | # App Check 2 | 3 | ```javascript 4 | import { ... } from 'react-firehooks/app-check'; 5 | ``` 6 | 7 | ## useAppCheckToken 8 | 9 | Returns and updates the current App Check token 10 | 11 | ```javascript 12 | const [user, loading, error] = useAppCheckToken(auth); 13 | ``` 14 | 15 | Params: 16 | 17 | - `appCheck`: Firebase App Check instance 18 | 19 | Returns: 20 | 21 | - `value`: App Check token; `undefined` if the token is currently being fetched, or an error occurred 22 | - `loading`: `true` while fetching the token; `false` if the token was fetched successfully or an error occurred 23 | - `error`: `undefined` if no error occurred 24 | -------------------------------------------------------------------------------- /docs/auth.md: -------------------------------------------------------------------------------- 1 | # Auth 2 | 3 | ```javascript 4 | import { ... } from 'react-firehooks/auth'; 5 | ``` 6 | 7 | ## useAuthIdToken 8 | 9 | Returns and updates the JWT of the currently authenticated user 10 | 11 | ```javascript 12 | const [idToken, loading, error] = useAuthIdToken(auth); 13 | ``` 14 | 15 | Params: 16 | 17 | - `auth`: Firebase Auth instance 18 | 19 | Returns: 20 | 21 | - `value`: JWT; `undefined` if the JWT is currently being fetched, or an error occurred 22 | - `loading`: `true` while fetching the JWT; `false` if the JWT was fetched successfully or an error occurred 23 | - `error`: `undefined` if no error occurred 24 | 25 | ## useAuthIdTokenResult 26 | 27 | Returns and updates the deserialized JWT of the currently authenticated user 28 | 29 | ```javascript 30 | const [idToken, loading, error] = useAuthIdTokenResult(auth); 31 | ``` 32 | 33 | Params: 34 | 35 | - `auth`: Firebase Auth instance 36 | 37 | Returns: 38 | 39 | - `value`: Deserialized JWT; `undefined` if the JWT is currently being fetched, or an error occurred 40 | - `loading`: `true` while fetching the JWT; `false` if the JWT was fetched successfully or an error occurred 41 | - `error`: `undefined` if no error occurred 42 | 43 | ## useAuthState 44 | 45 | Returns and updates the currently authenticated user 46 | 47 | ```javascript 48 | const [user, loading, error] = useAuthState(auth); 49 | ``` 50 | 51 | Params: 52 | 53 | - `auth`: Firebase Auth instance 54 | 55 | Returns: 56 | 57 | - `value`: User; `undefined` if the user is currently being fetched, or an error occurred 58 | - `loading`: `true` while fetching the user; `false` if the user was fetched successfully or an error occurred 59 | - `error`: `undefined` if no error occurred 60 | -------------------------------------------------------------------------------- /docs/database.md: -------------------------------------------------------------------------------- 1 | # Database 2 | 3 | ```javascript 4 | import { ... } from 'react-firehooks/database'; 5 | ``` 6 | 7 | ## useObject 8 | 9 | Returns and updates the DataSnapshot of the Realtime Database query 10 | 11 | ```javascript 12 | const [dataSnap, loading, error] = useObject(query); 13 | ``` 14 | 15 | Params: 16 | 17 | - `query`: Realtime Database query 18 | 19 | Returns: 20 | 21 | - `value`: DataSnapshot; `undefined` if query is currently being fetched, or an error occurred 22 | - `loading`: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 23 | - `error`: `undefined` if no error occurred 24 | 25 | ## useObjectOnce 26 | 27 | Returns the DataSnapshot of the Realtime Database query. Does not update the DataSnapshot once initially fetched 28 | 29 | ```javascript 30 | const [dataSnap, loading, error] = useObjectOnce(query); 31 | ``` 32 | 33 | Params: 34 | 35 | - `query`: Realtime Database query 36 | 37 | Returns: 38 | 39 | - `value`: DataSnapshot; `undefined` if query is currently being fetched, or an error occurred 40 | - `loading`: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 41 | - `error`: `undefined` if no error occurred 42 | 43 | ## useObjectValue 44 | 45 | Returns and updates the DataSnapshot of the Realtime Database query 46 | 47 | ```javascript 48 | const [objectValue, loading, error] = useObjectValue(query, options); 49 | ``` 50 | 51 | Params: 52 | 53 | - `query`: Realtime Database query 54 | - `options`: Options to configure how the object is fetched 55 | - `converter`: Function to extract the desired data from the DataSnapshot. Similar to Firestore converters. Default: `snap.val()`. 56 | 57 | Returns: 58 | 59 | - `value`: Object value; `undefined` if query is currently being fetched, or an error occurred 60 | - `loading`: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 61 | - `error`: `undefined` if no error occurred 62 | 63 | ## useObjectValueOnce 64 | 65 | Returns the DataSnapshot of the Realtime Database query. Does not update the DataSnapshot once initially fetched 66 | 67 | ```javascript 68 | const [objectValue, loading, error] = useObjectValueOnce(query, options); 69 | ``` 70 | 71 | Params: 72 | 73 | - `query`: Realtime Database query 74 | - `options`: Options to configure how the object is fetched 75 | - `converter`: Function to extract the desired data from the DataSnapshot. Similar to Firestore converters. Default: `snap.val()`. 76 | 77 | Returns: 78 | 79 | - `value`: Object value; `undefined` if query is currently being fetched, or an error occurred 80 | - `loading`: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 81 | - `error`: `undefined` if no error occurred 82 | -------------------------------------------------------------------------------- /docs/firestore.md: -------------------------------------------------------------------------------- 1 | # Firestore 2 | 3 | ```javascript 4 | import { ... } from 'react-firehooks/firestore'; 5 | ``` 6 | 7 | ## useAggregateFromServer 8 | 9 | Returns aggregate of a Firestore Query. Does not update the result once initially calculated. 10 | 11 | ```javascript 12 | const [data, loading, error] = useAggregateFromServer(query, aggregateSpec); 13 | ``` 14 | 15 | Params: 16 | 17 | - `query`: Firestore query the aggregate is calculated for 18 | - `aggregateSpec`: Aggregate specification 19 | 20 | Returns: 21 | 22 | - `value`: Aggregate of the Firestore query; `undefined` if the aggregate is currently being calculated, or an error occurred 23 | - `loading`: `true` while calculating the aggregate; `false` if the aggregate was calculated successfully or an error occurred 24 | - `error`: `undefined` if no error occurred 25 | 26 | ## useDocument 27 | 28 | Returns and updates a DocumentSnapshot of a Firestore DocumentReference 29 | 30 | ```javascript 31 | const [documentSnap, loading, error] = useDocument(documentReference, options); 32 | ``` 33 | 34 | Params: 35 | 36 | - `documentReference`: Firestore DocumentReference that will be subscribed to 37 | - `options`: Options to configure the subscription 38 | 39 | Returns: 40 | 41 | - `value`: DocumentSnapshot; `undefined` if document does not exist, is currently being fetched, or an error occurred 42 | - `loading`: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred 43 | - `error`: `undefined` if no error occurred 44 | 45 | ## useDocumentData 46 | 47 | Returns and updates the data of a Firestore DocumentReference 48 | 49 | ```javascript 50 | const [data, loading, error] = useDocumentData(documentReference, options); 51 | ``` 52 | 53 | Params: 54 | 55 | - `documentReference`: Firestore DocumentReference that will subscribed to 56 | - `options`: Options to configure the subscription 57 | 58 | Returns: 59 | 60 | - `value`: Document data; `undefined` if document does not exist, is currently being fetched, or an error occurred 61 | - `loading`: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred 62 | - `error`: `undefined` if no error occurred 63 | 64 | ## useDocumentDataOnce 65 | 66 | Returns the data of a Firestore DocumentReference 67 | 68 | ```javascript 69 | const [documentSnap, loading, error] = useDocumentDataOnce(documentReference, options); 70 | ``` 71 | 72 | Params: 73 | 74 | - `documentReference`: Firestore DocumentReference that will be fetched 75 | - `options`: Options to configure the document will be fetched 76 | 77 | Returns: 78 | 79 | - `value`: Document data; `undefined` if document does not exist, is currently being fetched, or an error occurred 80 | - `loading`: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred 81 | - `error`: `undefined` if no error occurred 82 | 83 | ## useDocumentOnce 84 | 85 | Returns the DocumentSnapshot of a Firestore DocumentReference. Does not update the DocumentSnapshot once initially fetched 86 | 87 | ```javascript 88 | const [querySnap, loading, error] = useDocumentData(documentReference, options); 89 | ``` 90 | 91 | Params: 92 | 93 | - `documentReference`: Firestore DocumentReference that will be fetched 94 | - `options`: Options to configure how the document will be fetched 95 | 96 | Returns: 97 | 98 | - `value`: DocumentSnapshot; `undefined` if document does not exist, is currently being fetched, or an error occurred 99 | - `loading`: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred 100 | - `error`: `undefined` if no error occurred 101 | 102 | ## useQueries 103 | 104 | Returns and updates a QuerySnapshot of multiple Firestore queries 105 | 106 | ```javascript 107 | const results = useQueries(queries, options); 108 | ``` 109 | 110 | Params: 111 | 112 | - `queries`: Firestore queries that will be subscribed to 113 | - `options`: Options to configure the subscription 114 | 115 | Returns: 116 | 117 | - Array with tuple for each query: 118 | - `value`: QuerySnapshot; `undefined` if query is currently being fetched, or an error occurred 119 | - `loading`: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 120 | - `error`: `undefined` if no error occurred 121 | 122 | ## useQueriesData 123 | 124 | Returns and updates a the document data of multiple Firestore queries 125 | 126 | ```javascript 127 | const results = useQueriesData(query, options); 128 | ``` 129 | 130 | Params: 131 | 132 | - `queries`: Firestore queries that will be subscribed to 133 | - `options`: Options to configure the subscription 134 | 135 | Returns: 136 | 137 | - Array with tuple for each query: 138 | - `value`: Query data; `undefined` if query is currently being fetched, or an error occurred 139 | - `loading` :`true` while fetching the query; `false` if the query was fetched successfully or an error occurred 140 | - `error`: `undefined` if no error occurred 141 | 142 | ## useQueriesDataOnce 143 | 144 | Returns the data of multiple Firestore queries 145 | 146 | ```javascript 147 | const results = useQueriesDataOnce(queries, options); 148 | ``` 149 | 150 | Params: 151 | 152 | - `queries`: Firestore queries that will be fetched 153 | - `options`: Options to configure how the queries are fetched 154 | 155 | Returns: 156 | 157 | - Array with tuple for each query:: 158 | - `value`: QuerySnapshot; `undefined` if query is currently being fetched, or an error occurred 159 | - `loading`: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 160 | - `error`: `undefined` if no error occurred 161 | 162 | ## useQueriesOnce 163 | 164 | Returns the QuerySnapshots of multiple Firestore queries 165 | 166 | ```javascript 167 | const results = useQueriesOnce(queries, options); 168 | ``` 169 | 170 | Params: 171 | 172 | - `queries`: Firestore queries that will be fetched 173 | - `options`: Options to configure how the queries are fetched 174 | 175 | Returns: 176 | 177 | - Array with tuple for each query:: 178 | - `value`: QuerySnapshot; `undefined` if query is currently being fetched, or an error occurred 179 | - `loading`: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 180 | - `error`: `undefined` if no error occurred 181 | 182 | ## useQuery 183 | 184 | Returns and updates a QuerySnapshot of a Firestore Query 185 | 186 | ```javascript 187 | const [querySnap, loading, error] = useQuery(query, options); 188 | ``` 189 | 190 | Params: 191 | 192 | - `query`: Firestore query that will be subscribed to 193 | - `options`: Options to configure the subscription 194 | 195 | Returns: 196 | 197 | - `value`: QuerySnapshot; `undefined` if query is currently being fetched, or an error occurred 198 | - `loading`: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 199 | - `error`: `undefined` if no error occurred 200 | 201 | ## useQueryData 202 | 203 | Returns and updates a the document data of a Firestore Query 204 | 205 | ```javascript 206 | const [data, loading, error] = useQueryData(query, options); 207 | ``` 208 | 209 | Params: 210 | 211 | - `query`: Firestore query that will be subscribed to 212 | - `options`: Options to configure the subscription 213 | 214 | Returns: 215 | 216 | - `value`: Query data; `undefined` if query is currently being fetched, or an error occurred 217 | - `loading`: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 218 | - `error`: `undefined` if no error occurred 219 | 220 | ## useQueryDataOnce 221 | 222 | Returns the data of a Firestore Query. Does not update the data once initially fetched 223 | 224 | ```javascript 225 | const [data, loading, error] = useQueryDataOnce(query, options); 226 | ``` 227 | 228 | Params: 229 | 230 | - `query`: Firestore query that will be fetched 231 | - `options`: Options to configure how the query is fetched 232 | 233 | Returns: 234 | 235 | - `value`: Query data; `undefined` if query is currently being fetched, or an error occurred 236 | - `loading`: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 237 | - `error`: `undefined` if no error occurred 238 | 239 | ## useQueryOnce 240 | 241 | Returns the QuerySnapshot of a Firestore Query. Does not update the QuerySnapshot once initially fetched 242 | 243 | ```javascript 244 | const [querySnap, loading, error] = useQueryOnce(query, options); 245 | ``` 246 | 247 | Params: 248 | 249 | - `query`: Firestore query that will be fetched 250 | - `options`: Options to configure how the query is fetched 251 | 252 | Returns: 253 | 254 | - `value`: QuerySnapshot; `undefined` if query is currently being fetched, or an error occurred 255 | - `loading`: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 256 | - `error`: `undefined` if no error occurred 257 | -------------------------------------------------------------------------------- /docs/message.md: -------------------------------------------------------------------------------- 1 | # Messaging 2 | 3 | ```javascript 4 | import { ... } from 'react-firehooks/messaging'; 5 | ``` 6 | 7 | ## useMessagingToken 8 | 9 | Returns the messaging token. The token never updates. 10 | 11 | ```javascript 12 | const [token, loading, error] = useMessagingToken(messaging, options); 13 | ``` 14 | 15 | Params: 16 | 17 | - `messaging`: Firestore Messaging instance 18 | - `options`: Options to configure how the token will be fetched 19 | 20 | Returns: 21 | 22 | - `value`: Messaging token; `undefined` if token is currently being fetched, or an error occurred 23 | - `loading`: `true` while fetching the token; `false` if the token was fetched successfully or an error occurred 24 | - `error`: `undefined` if no error occurred 25 | -------------------------------------------------------------------------------- /docs/storage.md: -------------------------------------------------------------------------------- 1 | # Storage 2 | 3 | ```javascript 4 | import { ... } from 'react-firehooks/storage'; 5 | ``` 6 | 7 | ## useBlob 8 | 9 | Returns the data of a Google Cloud Storage object as a Blob 10 | 11 | This hook is not available in Node. 12 | 13 | ```javascript 14 | const [data, loading, error] = useBlob(storageReference); 15 | ``` 16 | 17 | Params: 18 | 19 | - `reference`: Reference to a Google Cloud Storage object 20 | - `maxDownloadSizeBytes`: If set, the maximum allowed size in bytes to retrieve. 21 | 22 | Returns: 23 | 24 | - `value`: Object data as a Blob; `undefined` if data of the object is currently being downloaded, or an error occurred 25 | - `loading`: `true` while downloading the data of the object; `false` if the data was downloaded successfully or an error occurred 26 | - `error`: `undefined` if no error occurred 27 | 28 | ## useBytes 29 | 30 | Returns the data of a Google Cloud Storage object 31 | 32 | ```javascript 33 | const [data, loading, error] = useBytes(storageReference); 34 | ``` 35 | 36 | Params: 37 | 38 | - `reference`: Reference to a Google Cloud Storage object 39 | - `maxDownloadSizeBytes`: If set, the maximum allowed size in bytes to retrieve. 40 | 41 | Returns: 42 | 43 | - `value`: Object data; `undefined` if data of the object is currently being downloaded, or an error occurred 44 | - `loading`: `true` while downloading the data of the object; `false` if the data was downloaded successfully or an error occurred 45 | - `error`: `undefined` if no error occurred 46 | 47 | ## useDownloadURL 48 | 49 | Returns the download URL of a Google Cloud Storage object 50 | 51 | ```javascript 52 | const [url, loading, error] = useDownloadURL(storageReference); 53 | ``` 54 | 55 | Params: 56 | 57 | - `reference`: Reference to a Google Cloud Storage object 58 | 59 | Returns: 60 | 61 | - `value`: Download URL; `undefined` if download URL is currently being fetched, or an error occurred 62 | - `loading`: `true` while fetching the download URL; `false` if the download URL was fetched successfully or an error occurred 63 | - `error`: `undefined` if no error occurred 64 | 65 | ## useMetadata 66 | 67 | Returns the metadata of a Google Cloud Storage object 68 | 69 | ```javascript 70 | const [metadata, loading, error] = useMetadata(storageReference); 71 | ``` 72 | 73 | Params: 74 | 75 | - `reference`: Reference to a Google Cloud Storage object 76 | 77 | Returns: 78 | 79 | - `value`: Metadata; `undefined` if metadata is currently being fetched, or an error occurred 80 | - `loading`: `true` while fetching the metadata; `false` if the metadata was fetched successfully or an error occurred 81 | - `error`: `undefined` if no error occurred 82 | 83 | ## useStream 84 | 85 | Returns the data of a Google Cloud Storage object as a stream 86 | 87 | This hook is only available in Node. 88 | 89 | ```javascript 90 | const [data, loading, error] = useStream(storageReference); 91 | ``` 92 | 93 | Params: 94 | 95 | - `reference`: Reference to a Google Cloud Storage object 96 | - `maxDownloadSizeBytes`: If set, the maximum allowed size in bytes to retrieve. 97 | 98 | Returns: 99 | 100 | - `value`: Object data as a stream; `undefined` if data of the object is currently being downloaded, or an error occurred 101 | - `loading`: `true` while downloading the data of the object; `false` if the data was downloaded successfully or an error occurred 102 | - `error`: `undefined` if no error occurred 103 | -------------------------------------------------------------------------------- /migrations/react-firebase-hooks.md: -------------------------------------------------------------------------------- 1 | # Migrate from `react-firebase-hooks` 2 | 3 | This document explains the difference between [`react-firebase-hooks`](https://www.npmjs.com/package/react-firebase-hooks) and `react-firehooks`. 4 | 5 | ## Auth 6 | 7 | - `useCreateUserWithEmailAndPassword` was removed. Use `createUserWithEmailAndPassword` directly from `firebase/auth`. 8 | 9 | - `useSignInWithEmailAndPassword` was removed. Use `signInWithEmailAndPassword` directly from `firebase/auth`. 10 | 11 | ## Database 12 | 13 | - `useListKeys` was removed. Use `useObjectValue` with `options.converter`. 14 | - `useListVals` was removed. Use `useObjectValue` with `options.converter`. 15 | - `useObjectVal` was renamed to `useObjectValue` 16 | - `useObjectValOnce` was renamed to `useObjectValueOnce` 17 | - `options.keyField` was removed. Use `options.converter` instead. 18 | - `options.refField` was removed. Use `options.converter` instead. 19 | - `options.transform` was removed. Use `options.converter` instead. 20 | 21 | ## Firestore 22 | 23 | - `options.getOptions.source` was moved to `options.source` 24 | - `options.idField` was removed. Use [Firestore Converters](https://firebase.google.com/docs/reference/js/firestore_.firestoredataconverter) instead: 25 | 26 | ```javascript 27 | const ref = doc(firestore, "collection", "doc"); 28 | 29 | const converter = { 30 | toFirestore: (data) => data, 31 | fromFirestore: (snap) => ({ 32 | id: snap.id, 33 | ...snap.data(), 34 | }), 35 | }; 36 | 37 | const [data] = useDocumentData(ref.withConverter(converter)); 38 | ``` 39 | 40 | - `options.refField` was removed. Use [Firestore Converters](https://firebase.google.com/docs/reference/js/firestore_.firestoredataconverter) instead: 41 | 42 | ```javascript 43 | const ref = doc(firestore, "collection", "doc"); 44 | 45 | const converter = { 46 | toFirestore: (data) => data, 47 | fromFirestore: (snap) => ({ 48 | ref: snap.ref, 49 | ...snap.data(), 50 | }), 51 | }; 52 | 53 | const [data] = useDocumentData(ref.withConverter(converter)); 54 | ``` 55 | 56 | - `options.transform` was removed. Use [Firestore Converters](https://firebase.google.com/docs/reference/js/firestore_.firestoredataconverter) instead: 57 | 58 | ```javascript 59 | const ref = doc(firestore, "collection", "doc"); 60 | 61 | const converter = { 62 | toFirestore: (data) => data, 63 | fromFirestore: (snap) => transform(snap.data()), 64 | }; 65 | 66 | const [data] = useDocumentData(ref.withConverter(converter)); 67 | ``` 68 | 69 | ## Storage 70 | 71 | - No changes 72 | 73 | ## Other 74 | 75 | - The library doesn't include types for [Flow](https://flow.org/) 76 | -------------------------------------------------------------------------------- /migrations/v3.md: -------------------------------------------------------------------------------- 1 | # Migrate from v2 to v3 2 | 3 | ## Peer dependency 4 | 5 | This library now requires firebase 9.11.0 or later 6 | 7 | ## Firestore 8 | 9 | - `useCollection` was renamed to `useQuery` 10 | - `useCollectionData` was renamed to `useQueryData` 11 | 12 | ## Other 13 | 14 | The library is compiled to ES2017 instead of ES2015. 15 | -------------------------------------------------------------------------------- /migrations/v4.md: -------------------------------------------------------------------------------- 1 | # Migrate from v3 to v4 2 | 3 | ## Peer dependency 4 | 5 | This library now requires firebase 10.5.0 or later 6 | 7 | ## Firestore 8 | 9 | - `useCountFromServer` # Migrate from v3 to v4 10 | 11 | ## Peer dependency 12 | 13 | This library now requires firebase 10.5.0 or later 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-firehooks", 3 | "version": "4.2.0", 4 | "description": "Lightweight dependency-free collection of React hooks for Firebase", 5 | "main": "./lib/index.js", 6 | "types": "./lib/index.d.ts", 7 | "sideEffects": false, 8 | "type": "module", 9 | "exports": { 10 | ".": "./lib/index.js", 11 | "./app-check": "./lib/app-check/index.js", 12 | "./auth": "./lib/auth/index.js", 13 | "./database": "./lib/database/index.js", 14 | "./firestore": "./lib/firestore/index.js", 15 | "./messaging": "./lib/messaging/index.js", 16 | "./storage": "./lib/storage/index.js" 17 | }, 18 | "files": [ 19 | "lib", 20 | "auth", 21 | "app-check", 22 | "database", 23 | "firestore", 24 | "messaging", 25 | "storage" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/andipaetzold/react-firehooks.git" 30 | }, 31 | "keywords": [ 32 | "react", 33 | "hooks", 34 | "firebase", 35 | "app-check", 36 | "auth", 37 | "database", 38 | "firestore", 39 | "messaging", 40 | "storage" 41 | ], 42 | "author": { 43 | "name": "Andi Pätzold", 44 | "email": "github@andipaetzold.com", 45 | "url": "https://github.com/andipaetzold" 46 | }, 47 | "license": "MIT", 48 | "bugs": { 49 | "url": "https://github.com/andipaetzold/react-firehooks/issues" 50 | }, 51 | "homepage": "https://github.com/andipaetzold/react-firehooks#readme", 52 | "devDependencies": { 53 | "@semantic-release/changelog": "6.0.3", 54 | "@semantic-release/git": "10.0.1", 55 | "@testing-library/react": "16.0.1", 56 | "@tsconfig/recommended": "1.0.7", 57 | "@tsconfig/strictest": "2.0.5", 58 | "@types/react": "18.3.11", 59 | "@typescript-eslint/eslint-plugin": "7.18.0", 60 | "@typescript-eslint/parser": "7.18.0", 61 | "@vitest/coverage-v8": "2.1.2", 62 | "eslint": "8.57.1", 63 | "eslint-config-prettier": "9.1.0", 64 | "eslint-plugin-jsdoc": "50.3.1", 65 | "eslint-plugin-react": "7.37.1", 66 | "eslint-plugin-react-hooks": "4.6.2", 67 | "firebase": "10.5.0", 68 | "happy-dom": "15.10.2", 69 | "husky": "9.1.6", 70 | "lint-staged": "15.2.10", 71 | "prettier": "3.3.3", 72 | "react": "18.3.1", 73 | "react-test-renderer": "18.3.1", 74 | "rimraf": "6.0.1", 75 | "semantic-release": "24.1.2", 76 | "typedoc": "0.26.8", 77 | "typescript": "5.6.3", 78 | "vitest": "2.1.2" 79 | }, 80 | "scripts": { 81 | "prepublishOnly": "npm run build", 82 | "build": "npm run build:esm && npm run build:modules", 83 | "build:esm": "rimraf lib && tsc", 84 | "build:modules": "node ./scripts/create-modules.js", 85 | "test": "vitest", 86 | "typecheck": "vitest --typecheck", 87 | "semantic-release": "semantic-release", 88 | "typedoc": "typedoc", 89 | "prepare": "husky", 90 | "lint": "eslint src", 91 | "lint-staged": "lint-staged" 92 | }, 93 | "peerDependencies": { 94 | "firebase": "^10.5.0 || ^11.0.0", 95 | "react": ">=16.8.0" 96 | }, 97 | "lint-staged": { 98 | "src/**/*.ts": [ 99 | "prettier --write", 100 | "eslint" 101 | ] 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /scripts/create-modules.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | const modules = ["app-check", "auth", "database", "firestore", "messaging", "storage"]; 4 | for (const module of modules) { 5 | const packageJson = { 6 | main: `../lib/${module}/index.js`, 7 | types: `../lib/${module}/index.d.ts`, 8 | sideEffects: false, 9 | }; 10 | 11 | if (!fs.existsSync(module)) { 12 | fs.mkdirSync(module); 13 | } 14 | fs.writeFileSync(`${module}/package.json`, JSON.stringify(packageJson, undefined, 4)); 15 | } 16 | -------------------------------------------------------------------------------- /src/__testfixtures__/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./newPromise.js"; 2 | export * from "./newSymbol.js"; 3 | -------------------------------------------------------------------------------- /src/__testfixtures__/newPromise.ts: -------------------------------------------------------------------------------- 1 | export function newPromise() { 2 | let resolve: (value: T) => void; 3 | let reject: (error: unknown) => void; 4 | const promise = new Promise((_resolve, _reject) => { 5 | resolve = _resolve; 6 | reject = _reject; 7 | }); 8 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 9 | // @ts-ignore 10 | return { promise, resolve, reject }; 11 | } 12 | -------------------------------------------------------------------------------- /src/__testfixtures__/newSymbol.ts: -------------------------------------------------------------------------------- 1 | export function newSymbol(name: string): T { 2 | return Symbol(name) as T; 3 | } 4 | -------------------------------------------------------------------------------- /src/app-check/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useAppCheckToken.js"; 2 | -------------------------------------------------------------------------------- /src/app-check/useAppCheckToken.ts: -------------------------------------------------------------------------------- 1 | import { AppCheck, AppCheckTokenResult, onTokenChanged } from "firebase/app-check"; 2 | import { ValueHookResult } from "../common/index.js"; 3 | import { useListen, UseListenOnChange } from "../internal/useListen.js"; 4 | import { LoadingState } from "../internal/useLoadingValue.js"; 5 | 6 | export type UseAppCheckToken = ValueHookResult; 7 | 8 | const onChange: UseListenOnChange = (stableAppCheck, next, error) => 9 | onTokenChanged(stableAppCheck, next, error); 10 | 11 | /** 12 | * Returns and updates the current App Check token 13 | * @param appCheck Firebase App Check instance 14 | * @returns App Check token, loading state, and error 15 | * - value: App Check token; `undefined` if token is currently being fetched, or an error occurred 16 | * - loading: `true` while fetching the token; `false` if the token was fetched successfully or an error occurred 17 | * - error: `undefined` if no error occurred 18 | */ 19 | export function useAppCheckToken(appCheck: AppCheck): UseAppCheckToken { 20 | return useListen(appCheck, onChange, () => true, LoadingState); 21 | } 22 | -------------------------------------------------------------------------------- /src/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useAuthIdToken.js"; 2 | export * from "./useAuthIdTokenResult.js"; 3 | export * from "./useAuthState.js"; 4 | -------------------------------------------------------------------------------- /src/auth/useAuthIdToken.ts: -------------------------------------------------------------------------------- 1 | import { Auth, AuthError, getIdToken, onIdTokenChanged } from "firebase/auth"; 2 | import { ValueHookResult } from "../common/index.js"; 3 | import { UseListenOnChange, useListen } from "../internal/useListen.js"; 4 | import { LoadingState } from "../internal/useLoadingValue.js"; 5 | 6 | export type UseAuthIdTokenResult = ValueHookResult; 7 | 8 | const onChange: UseListenOnChange = (stableAuth, next, error) => 9 | onIdTokenChanged(stableAuth, async (user) => { 10 | if (user) { 11 | try { 12 | // Can also be accessed via `user.accessToken`, but that's not officially documented 13 | const idToken = await getIdToken(user); 14 | next(idToken); 15 | } catch (e) { 16 | error(e as AuthError); 17 | } 18 | } else { 19 | next(null); 20 | } 21 | }); 22 | 23 | /** 24 | * Returns and updates the JWT of the currently authenticated user 25 | * @param auth Firebase Auth instance 26 | * @returns JWT, loading state, and error 27 | * - value: JWT; `undefined` if the JWT is currently being fetched, or an error occurred 28 | * - loading: `true` while fetching the JWT; `false` if the JWT was fetched successfully or an error occurred 29 | * - error: `undefined` if no error occurred 30 | */ 31 | export function useAuthIdToken(auth: Auth): UseAuthIdTokenResult { 32 | return useListen(auth, onChange, () => true, LoadingState); 33 | } 34 | -------------------------------------------------------------------------------- /src/auth/useAuthIdTokenResult.ts: -------------------------------------------------------------------------------- 1 | import { Auth, AuthError, getIdTokenResult, IdTokenResult, onIdTokenChanged } from "firebase/auth"; 2 | import { ValueHookResult } from "../common/index.js"; 3 | import { useListen, UseListenOnChange } from "../internal/useListen.js"; 4 | import { LoadingState } from "../internal/useLoadingValue.js"; 5 | 6 | export type UseAuthIdTokenResultResult = ValueHookResult; 7 | 8 | const onChange: UseListenOnChange = (stableAuth, next, error) => 9 | onIdTokenChanged(stableAuth, async (user) => { 10 | if (user) { 11 | try { 12 | // Can also be accessed via `user.accessToken`, but that's not officially documented 13 | const idTokenResult = await getIdTokenResult(user); 14 | next(idTokenResult); 15 | } catch (e) { 16 | error(e as AuthError); 17 | } 18 | } else { 19 | next(null); 20 | } 21 | }); 22 | 23 | /** 24 | * Returns and updates the deserialized JWT of the currently authenticated user 25 | * @param auth Firebase Auth instance 26 | * @returns Deserialized JWT, loading state, and error 27 | * - value: Deserialized JWT; `undefined` if the JWT is currently being fetched, or an error occurred 28 | * - loading: `true` while fetching JWT; `false` if the JWT was fetched successfully or an error occurred 29 | * - error: `undefined` if no error occurred 30 | */ 31 | export function useAuthIdTokenResult(auth: Auth): UseAuthIdTokenResultResult { 32 | return useListen(auth, onChange, () => true, LoadingState); 33 | } 34 | -------------------------------------------------------------------------------- /src/auth/useAuthState.spec.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from "@testing-library/react"; 2 | import { Auth, onAuthStateChanged, User } from "firebase/auth"; 3 | import { beforeEach, describe, expect, it, vi } from "vitest"; 4 | import { newSymbol } from "../__testfixtures__/index.js"; 5 | import { useAuthState } from "./useAuthState.js"; 6 | 7 | vi.mock("firebase/auth", () => ({ 8 | onAuthStateChanged: vi.fn(), 9 | })); 10 | 11 | beforeEach(() => { 12 | vi.resetAllMocks(); 13 | }); 14 | 15 | describe("initial state", () => { 16 | it("should return currentUser when defined", () => { 17 | const currentUser = newSymbol("Current User"); 18 | const mockAuth = { currentUser } as Auth; 19 | 20 | vi.mocked(onAuthStateChanged).mockImplementation(() => () => {}); 21 | 22 | const { result } = renderHook(() => useAuthState(mockAuth)); 23 | expect(result.current).toStrictEqual([currentUser, false, undefined]); 24 | }); 25 | 26 | it("should return undefined when currentUser is null", () => { 27 | const mockAuth = { currentUser: null } as Auth; 28 | 29 | vi.mocked(onAuthStateChanged).mockImplementation(() => () => {}); 30 | 31 | const { result } = renderHook(() => useAuthState(mockAuth)); 32 | expect(result.current).toStrictEqual([undefined, true, undefined]); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/auth/useAuthState.ts: -------------------------------------------------------------------------------- 1 | import { Auth, AuthError, onAuthStateChanged, User } from "firebase/auth"; 2 | import { ValueHookResult } from "../common/index.js"; 3 | import { useListen, UseListenOnChange } from "../internal/useListen.js"; 4 | import { LoadingState } from "../internal/useLoadingValue.js"; 5 | 6 | export type UseAuthStateResult = ValueHookResult; 7 | 8 | const onChange: UseListenOnChange = (stableAuth, next, error) => 9 | onAuthStateChanged(stableAuth, next, (e) => error(e as AuthError)); 10 | 11 | /** 12 | * Returns and updates the currently authenticated user 13 | * @param auth Firebase Auth instance 14 | * @returns User, loading state, and error 15 | * - value: User; `undefined` if user is currently being fetched, or an error occurred 16 | * - loading: `true` while fetching the user; `false` if the user was fetched successfully or an error occurred 17 | * - error: `undefined` if no error occurred 18 | */ 19 | export function useAuthState(auth: Auth): UseAuthStateResult { 20 | return useListen(auth, onChange, () => true, auth.currentUser ? auth.currentUser : LoadingState); 21 | } 22 | -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | export type * from "./types.js"; 2 | -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- 1 | export type ValueHookResult = [value: Value | undefined, loading: boolean, error: Error | undefined]; 2 | -------------------------------------------------------------------------------- /src/database/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useObject.js"; 2 | export * from "./useObjectOnce.js"; 3 | export * from "./useObjectValue.js"; 4 | export * from "./useObjectValueOnce.js"; 5 | -------------------------------------------------------------------------------- /src/database/internal.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsdoc/require-returns */ 2 | /* eslint-disable jsdoc/require-param */ 3 | import type { DataSnapshot, Query } from "firebase/database"; 4 | 5 | /** 6 | * @internal 7 | */ 8 | export function isQueryEqual(a: Query | undefined, b: Query | undefined): boolean { 9 | const areBothUndefined = a === undefined && b === undefined; 10 | const areSameRef = a !== undefined && b !== undefined && a.isEqual(b); 11 | return areBothUndefined || areSameRef; 12 | } 13 | 14 | export const defaultConverter = (snap: DataSnapshot) => snap.val(); 15 | -------------------------------------------------------------------------------- /src/database/useObject.ts: -------------------------------------------------------------------------------- 1 | import { DataSnapshot, onValue, Query } from "firebase/database"; 2 | import type { ValueHookResult } from "../common/index.js"; 3 | import { useListen } from "../internal/useListen.js"; 4 | import { LoadingState } from "../internal/useLoadingValue.js"; 5 | import { isQueryEqual } from "./internal.js"; 6 | 7 | export type UseObjectResult = ValueHookResult; 8 | 9 | /** 10 | * Returns the DataSnapshot of the Realtime Database query 11 | * @param query Realtime Database query 12 | * @returns User, loading state, and error 13 | * - value: DataSnapshot; `undefined` if query is currently being fetched, or an error occurred 14 | * - loading: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 15 | * - error: `undefined` if no error occurred 16 | */ 17 | export function useObject(query: Query | undefined | null): UseObjectResult { 18 | return useListen(query ?? undefined, onValue, isQueryEqual, LoadingState); 19 | } 20 | -------------------------------------------------------------------------------- /src/database/useObjectOnce.ts: -------------------------------------------------------------------------------- 1 | import { DataSnapshot, get, Query } from "firebase/database"; 2 | import { useCallback } from "react"; 3 | import type { ValueHookResult } from "../common/index.js"; 4 | import { useGet } from "../internal/useGet.js"; 5 | import { isQueryEqual } from "./internal.js"; 6 | 7 | export type UseObjectOnceResult = ValueHookResult; 8 | 9 | /** 10 | * Returns and updates the DataSnapshot of the Realtime Database query. Does not update the DataSnapshot once initially fetched 11 | * @param query Realtime Database query 12 | * @returns User, loading state, and error 13 | * - value: DataSnapshot; `undefined` if query is currently being fetched, or an error occurred 14 | * - loading: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 15 | * - error: `undefined` if no error occurred 16 | */ 17 | export function useObjectOnce(query: Query | undefined | null): UseObjectOnceResult { 18 | const getData = useCallback((stableQuery: Query) => get(stableQuery), []); 19 | return useGet(query ?? undefined, getData, isQueryEqual); 20 | } 21 | -------------------------------------------------------------------------------- /src/database/useObjectValue.ts: -------------------------------------------------------------------------------- 1 | import { DataSnapshot, onValue, Query } from "firebase/database"; 2 | import { useCallback } from "react"; 3 | import type { ValueHookResult } from "../common/index.js"; 4 | import { useListen, UseListenOnChange } from "../internal/useListen.js"; 5 | import { LoadingState } from "../internal/useLoadingValue.js"; 6 | import { defaultConverter, isQueryEqual } from "./internal.js"; 7 | 8 | export type UseObjectValueResult = ValueHookResult; 9 | 10 | export type UseObjectValueConverter = (snap: DataSnapshot) => Value; 11 | 12 | export interface UseObjectValueOptions { 13 | converter?: UseObjectValueConverter | undefined; 14 | initialValue?: Value | undefined; 15 | } 16 | 17 | /** 18 | * Returns and updates the DataSnapshot of the Realtime Database query 19 | * @template Value Type of the object value 20 | * @param query Realtime Database query 21 | * @param options Options to configure how the object is fetched 22 | * `converter`: Function to extract the desired data from the DataSnapshot. Similar to Firestore converters. Default: `snap.val()`. 23 | * `initialValue`: Value that is returned while the object is being fetched. 24 | * @returns User, loading state, and error 25 | * - value: Object value; `undefined` if query is currently being fetched, or an error occurred 26 | * - loading: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 27 | * - error: `undefined` if no error occurred 28 | */ 29 | export function useObjectValue( 30 | query: Query | undefined | null, 31 | options?: UseObjectValueOptions | undefined, 32 | ): UseObjectValueResult { 33 | const { converter = defaultConverter, initialValue = LoadingState } = options ?? {}; 34 | 35 | const onChange: UseListenOnChange = useCallback( 36 | (stableQuery, next, error) => onValue(stableQuery, (snap) => next(converter(snap)), error), 37 | // TODO: add options as dependency 38 | // eslint-disable-next-line react-hooks/exhaustive-deps 39 | [], 40 | ); 41 | 42 | return useListen(query ?? undefined, onChange, isQueryEqual, initialValue); 43 | } 44 | -------------------------------------------------------------------------------- /src/database/useObjectValueOnce.ts: -------------------------------------------------------------------------------- 1 | import { DataSnapshot, get, Query } from "firebase/database"; 2 | import { useCallback } from "react"; 3 | import type { ValueHookResult } from "../common/index.js"; 4 | import { useGet } from "../internal/useGet.js"; 5 | import { defaultConverter, isQueryEqual } from "./internal.js"; 6 | 7 | export type UseObjectValueOnceResult = ValueHookResult; 8 | 9 | export type UseObjectValueOnceConverter = (snap: DataSnapshot) => Value; 10 | 11 | export interface UseObjectValueOnceOptions { 12 | converter?: UseObjectValueOnceConverter | undefined; 13 | } 14 | 15 | /** 16 | * Returns the DataSnapshot of the Realtime Database query. Does not update the DataSnapshot once initially fetched 17 | * @template Value Type of the object value 18 | * @param query Realtime Database query 19 | * @param options Options to configure how the object is fetched 20 | * `converter`: Function to extract the desired data from the DataSnapshot. Similar to Firestore converters. Default: `snap.val()`. 21 | * @returns User, loading state, and error 22 | * - value: Object value; `undefined` if query is currently being fetched, or an error occurred 23 | * - loading: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 24 | * - error: `undefined` if no error occurred 25 | */ 26 | export function useObjectValueOnce( 27 | query: Query | undefined | null, 28 | options?: UseObjectValueOnceOptions | undefined, 29 | ): UseObjectValueOnceResult { 30 | const { converter = defaultConverter } = options ?? {}; 31 | 32 | const getData = useCallback(async (stableQuery: Query) => { 33 | const snap = await get(stableQuery); 34 | return converter(snap); 35 | // TODO: add options as dependency 36 | // eslint-disable-next-line react-hooks/exhaustive-deps 37 | }, []); 38 | 39 | return useGet(query ?? undefined, getData, isQueryEqual); 40 | } 41 | -------------------------------------------------------------------------------- /src/firestore/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types.js"; 2 | export * from "./useAggregateFromServer.js"; 3 | export * from "./useDocument.js"; 4 | export * from "./useDocumentData.js"; 5 | export * from "./useDocumentDataOnce.js"; 6 | export * from "./useDocumentOnce.js"; 7 | export * from "./useQueries.js"; 8 | export * from "./useQueriesData.js"; 9 | export * from "./useQueriesDataOnce.js"; 10 | export * from "./useQuery.js"; 11 | export * from "./useQueryData.js"; 12 | export * from "./useQueryDataOnce.js"; 13 | export * from "./useQueryOnce.js"; 14 | -------------------------------------------------------------------------------- /src/firestore/internal.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | average, 3 | count, 4 | DocumentReference, 5 | getDoc, 6 | getDocFromCache, 7 | getDocFromServer, 8 | getDocs, 9 | getDocsFromCache, 10 | getDocsFromServer, 11 | Query, 12 | } from "firebase/firestore"; 13 | import { beforeEach, describe, expect, it, vi } from "vitest"; 14 | import { newSymbol } from "../__testfixtures__/index.js"; 15 | import { getDocFromSource, getDocsFromSource, isAggregateSpecEqual, isDocRefEqual, isQueryEqual } from "./internal.js"; 16 | 17 | vi.mock("firebase/firestore", async () => { 18 | const mod = await vi.importActual("firebase/firestore"); 19 | 20 | return { 21 | ...mod, 22 | getDoc: vi.fn(), 23 | getDocFromServer: vi.fn(), 24 | getDocFromCache: vi.fn(), 25 | getDocs: vi.fn(), 26 | getDocsFromServer: vi.fn(), 27 | getDocsFromCache: vi.fn(), 28 | queryEqual: Object.is, 29 | refEqual: Object.is, 30 | }; 31 | }); 32 | 33 | beforeEach(() => { 34 | vi.resetAllMocks(); 35 | }); 36 | 37 | describe("getDocFromSource", () => { 38 | it("uses default getDoc method", async () => { 39 | const reference = newSymbol("Reference"); 40 | 41 | await getDocFromSource(reference, "default"); 42 | 43 | expect(vi.mocked(getDoc)).toHaveBeenCalledWith(reference); 44 | }); 45 | 46 | it("uses cache getDoc method", async () => { 47 | const reference = newSymbol("Reference"); 48 | 49 | await getDocFromSource(reference, "cache"); 50 | 51 | expect(vi.mocked(getDocFromCache)).toHaveBeenCalledWith(reference); 52 | }); 53 | 54 | it("uses server getDoc method", async () => { 55 | const reference = newSymbol("Reference"); 56 | 57 | await getDocFromSource(reference, "server"); 58 | 59 | expect(vi.mocked(getDocFromServer)).toHaveBeenCalledWith(reference); 60 | }); 61 | }); 62 | 63 | describe("getDocsFromSource", () => { 64 | it("uses default getDocs method", async () => { 65 | const query = newSymbol("Query"); 66 | 67 | await getDocsFromSource(query, "default"); 68 | 69 | expect(vi.mocked(getDocs)).toHaveBeenCalledWith(query); 70 | }); 71 | 72 | it("uses cache getDocs method", async () => { 73 | const query = newSymbol("Query"); 74 | 75 | await getDocsFromSource(query, "cache"); 76 | 77 | expect(vi.mocked(getDocsFromCache)).toHaveBeenCalledWith(query); 78 | }); 79 | 80 | it("uses server getDocs method", async () => { 81 | const query = newSymbol("Query"); 82 | 83 | await getDocsFromSource(query, "server"); 84 | 85 | expect(vi.mocked(getDocsFromServer)).toHaveBeenCalledWith(query); 86 | }); 87 | }); 88 | 89 | describe("isDocRefEqual", () => { 90 | it("with undefined", () => { 91 | const ref = newSymbol("ref"); 92 | expect(isDocRefEqual(undefined, undefined)).toBe(true); 93 | expect(isDocRefEqual(ref, undefined)).toBe(false); 94 | expect(isDocRefEqual(undefined, ref)).toBe(false); 95 | }); 96 | 97 | it("with refs", () => { 98 | const refA = newSymbol("refA"); 99 | const refB = newSymbol("refB"); 100 | expect(isDocRefEqual(refA, refA)).toBe(true); 101 | expect(isDocRefEqual(refA, refB)).toBe(false); 102 | }); 103 | }); 104 | 105 | describe("isQueryEqual", () => { 106 | it("with undefined", () => { 107 | const query = newSymbol("query"); 108 | expect(isQueryEqual(undefined, undefined)).toBe(true); 109 | expect(isQueryEqual(query, undefined)).toBe(false); 110 | expect(isQueryEqual(undefined, query)).toBe(false); 111 | }); 112 | 113 | it("with refs", () => { 114 | const queryA = newSymbol("queryA"); 115 | const queryB = newSymbol("queryB"); 116 | expect(isQueryEqual(queryA, queryA)).toBe(true); 117 | expect(isQueryEqual(queryA, queryB)).toBe(false); 118 | }); 119 | }); 120 | 121 | describe("isAggregateSpecEqual", () => { 122 | it("different key count", () => { 123 | const spec1 = { count: count() }; 124 | const spec2 = {}; 125 | expect(isAggregateSpecEqual(spec1, spec2)).toBe(false); 126 | }); 127 | 128 | it("different keys", () => { 129 | const spec1 = { count: count() }; 130 | const spec2 = { total: count() }; 131 | // @ts-expect-error Different specs 132 | expect(isAggregateSpecEqual(spec1, spec2)).toBe(false); 133 | }); 134 | 135 | it("different aggregations", () => { 136 | const spec1 = { value: count() }; 137 | const spec2 = { value: average("abc") }; 138 | expect(isAggregateSpecEqual(spec1, spec2)).toBe(false); 139 | }); 140 | 141 | it("identical, single aggregate", () => { 142 | const spec1 = { count: count() }; 143 | const spec2 = { count: count() }; 144 | expect(isAggregateSpecEqual(spec1, spec2)).toBe(true); 145 | }); 146 | 147 | it("identical, multiple aggregates", () => { 148 | const spec1 = { count: count(), avg: average("123") }; 149 | const spec2 = { count: count(), avg: average("123") }; 150 | expect(isAggregateSpecEqual(spec1, spec2)).toBe(true); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /src/firestore/internal.ts: -------------------------------------------------------------------------------- 1 | import { 2 | aggregateFieldEqual, 3 | AggregateSpec, 4 | DocumentData, 5 | DocumentReference, 6 | DocumentSnapshot, 7 | getDoc, 8 | getDocFromCache, 9 | getDocFromServer, 10 | getDocs, 11 | getDocsFromCache, 12 | getDocsFromServer, 13 | Query, 14 | queryEqual, 15 | QuerySnapshot, 16 | refEqual, 17 | SnapshotListenOptions, 18 | } from "firebase/firestore"; 19 | import type { Source } from "./types.js"; 20 | 21 | /** 22 | * @since 10.9.0 23 | */ 24 | export type ListenSource = "default" | "cache"; 25 | 26 | /** 27 | * This library currently supports firebase@10.5.0 which is missing the `source` property in `SnapshotListenOptions`. 28 | */ 29 | export type SnapshotListenOptionsInternal = SnapshotListenOptions & { 30 | readonly source?: "default" | "cache"; 31 | }; 32 | 33 | /** 34 | * @internal 35 | */ 36 | export async function getDocFromSource( 37 | reference: DocumentReference, 38 | source: Source, 39 | ): Promise> { 40 | switch (source) { 41 | case "cache": 42 | return await getDocFromCache(reference); 43 | case "server": 44 | return await getDocFromServer(reference); 45 | case "default": 46 | return await getDoc(reference); 47 | } 48 | } 49 | 50 | /** 51 | * @internal 52 | */ 53 | export async function getDocsFromSource( 54 | query: Query, 55 | source: Source, 56 | ): Promise> { 57 | switch (source) { 58 | case "cache": 59 | return await getDocsFromCache(query); 60 | case "server": 61 | return await getDocsFromServer(query); 62 | case "default": 63 | return await getDocs(query); 64 | } 65 | } 66 | 67 | /** 68 | * @internal 69 | */ 70 | export function isDocRefEqual( 71 | a: DocumentReference | undefined, 72 | b: DocumentReference | undefined, 73 | ): boolean { 74 | const areBothUndefined = a === undefined && b === undefined; 75 | const areSameRef = a !== undefined && b !== undefined && refEqual(a, b); 76 | return areBothUndefined || areSameRef; 77 | } 78 | 79 | /** 80 | * @internal 81 | */ 82 | export function isQueryEqual( 83 | a: Query | undefined, 84 | b: Query | undefined, 85 | ): boolean { 86 | const areBothUndefined = a === undefined && b === undefined; 87 | const areSameRef = a !== undefined && b !== undefined && queryEqual(a, b); 88 | return areBothUndefined || areSameRef; 89 | } 90 | 91 | /** 92 | * @internal 93 | */ 94 | export function isAggregateSpecEqual(a: T, b: T): boolean { 95 | if (Object.keys(a).length !== Object.keys(b).length) { 96 | return false; 97 | } 98 | 99 | return Object.entries(a).every(([key, value]) => aggregateFieldEqual(value, b[key]!)); 100 | } 101 | -------------------------------------------------------------------------------- /src/firestore/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Specifies how the document or query should be fetched 3 | * 4 | * `default`: Attempts to provide up-to-date data when possible by waiting for data from the server, but it may return cached data or fail if you are offline and the server cannot be reached. 5 | * `server`: Reads the document/query from the server. Returns an error if the network is not available. 6 | * `cache`: Reads the document/query from cache. Returns an error if the document/query is not currently cached. 7 | */ 8 | export type Source = "default" | "server" | "cache"; 9 | -------------------------------------------------------------------------------- /src/firestore/useAggregateFromServer.ts: -------------------------------------------------------------------------------- 1 | import type { AggregateSpec, AggregateSpecData, FirestoreError, Query } from "firebase/firestore"; 2 | import { getAggregateFromServer } from "firebase/firestore"; 3 | import type { ValueHookResult } from "../common/types.js"; 4 | import { useGet } from "../internal/useGet.js"; 5 | import { isAggregateSpecEqual, isQueryEqual } from "./internal.js"; 6 | 7 | export type UseAggregateFromServerResult = ValueHookResult, FirestoreError>; 8 | 9 | interface Reference { 10 | query: Query; 11 | aggregateSpec: T; 12 | } 13 | 14 | // eslint-disable-next-line jsdoc/require-param, jsdoc/require-returns 15 | /** 16 | * @internal 17 | */ 18 | async function getData({ query, aggregateSpec }: Reference): Promise> { 19 | const snap = await getAggregateFromServer(query, aggregateSpec); 20 | return snap.data(); 21 | } 22 | 23 | // eslint-disable-next-line jsdoc/require-param, jsdoc/require-returns 24 | /** 25 | * @internal 26 | */ 27 | function isEqual>( 28 | a: TReference | undefined, 29 | b: TReference | undefined, 30 | ): boolean { 31 | if (a === undefined && b === undefined) { 32 | return true; 33 | } 34 | 35 | const areSameRef = 36 | a !== undefined && 37 | b !== undefined && 38 | isQueryEqual(a.query, b.query) && 39 | isAggregateSpecEqual(a.aggregateSpec, a.aggregateSpec); 40 | return areSameRef; 41 | } 42 | 43 | /** 44 | * Returns aggregate of a Firestore Query. Does not update the result once initially calculated. 45 | * @param query Firestore query the aggregate is calculated for 46 | * @param aggregateSpec Aggregate specification 47 | * @returns Size of the result set, loading state, and error 48 | * - value: Aggregate of the Firestore query; `undefined` if the aggregate is currently being calculated, or an error occurred 49 | * - loading: `true` while calculating the aggregate; `false` if the aggregate was calculated successfully or an error occurred 50 | * - error: `undefined` if no error occurred 51 | */ 52 | export function useAggregateFromServer( 53 | query: Query | undefined | null, 54 | aggregateSpec: T, 55 | ): UseAggregateFromServerResult { 56 | return useGet(query ? { query, aggregateSpec } : undefined, getData, isEqual); 57 | } 58 | -------------------------------------------------------------------------------- /src/firestore/useDocument.spec.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from "@testing-library/react"; 2 | import { expect, it, vi, describe, beforeEach } from "vitest"; 3 | import { useDocument } from "./useDocument.js"; 4 | import { newSymbol } from "../__testfixtures__/index.js"; 5 | import { DocumentReference, onSnapshot } from "firebase/firestore"; 6 | 7 | vi.mock("firebase/firestore", async () => ({ 8 | ...(await vi.importActual("firebase/firestore")), 9 | onSnapshot: vi.fn().mockReturnValue(vi.fn()), 10 | })); 11 | 12 | beforeEach(() => { 13 | vi.clearAllMocks(); 14 | }); 15 | 16 | const ref1 = newSymbol("Ref"); 17 | 18 | describe("when re-rendered", () => { 19 | it("with equal, but non-identical options object", () => { 20 | const options1 = { snapshotListenOptions: { includeMetadataChanges: false } }; 21 | const options2 = { snapshotListenOptions: { includeMetadataChanges: false } }; 22 | 23 | const { rerender } = renderHook(({ options }) => useDocument(ref1, options), { 24 | initialProps: { options: options1 }, 25 | }); 26 | 27 | expect(onSnapshot).toHaveBeenCalledTimes(1); 28 | rerender({ options: options2 }); 29 | expect(onSnapshot).toHaveBeenCalledTimes(1); 30 | }); 31 | 32 | it("with different options object", () => { 33 | const options1 = { snapshotListenOptions: { includeMetadataChanges: false } }; 34 | const options2 = { snapshotListenOptions: { includeMetadataChanges: true } }; 35 | 36 | const { rerender } = renderHook(({ options }) => useDocument(ref1, options), { 37 | initialProps: { options: options1 }, 38 | }); 39 | 40 | expect(onSnapshot).toHaveBeenCalledTimes(1); 41 | rerender({ options: options2 }); 42 | expect(onSnapshot).toHaveBeenCalledTimes(2); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/firestore/useDocument.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DocumentData, 3 | DocumentReference, 4 | DocumentSnapshot, 5 | FirestoreError, 6 | onSnapshot, 7 | SnapshotListenOptions, 8 | } from "firebase/firestore"; 9 | import { useCallback } from "react"; 10 | import type { ValueHookResult } from "../common/types.js"; 11 | import { useListen, UseListenOnChange } from "../internal/useListen.js"; 12 | import { LoadingState } from "../internal/useLoadingValue.js"; 13 | import { isDocRefEqual, SnapshotListenOptionsInternal } from "./internal.js"; 14 | 15 | export type UseDocumentResult = ValueHookResult, FirestoreError>; 16 | 17 | /** 18 | * Options to configure the subscription 19 | */ 20 | export interface UseDocumentOptions { 21 | snapshotListenOptions?: SnapshotListenOptions | undefined; 22 | } 23 | 24 | /** 25 | * Returns and updates a DocumentSnapshot of a Firestore DocumentReference 26 | * @template AppModelType Shape of the data after it was converted from firestore 27 | * @template DbModelType Shape of the data in firestore 28 | * @param reference Firestore DocumentReference that will be subscribed to 29 | * @param options Options to configure the subscription 30 | * @returns Document snapshot, loading state, and error 31 | * - value: DocumentSnapshot; `undefined` if document does not exist, is currently being fetched, or an error occurred 32 | * - loading: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred 33 | * - error: `undefined` if no error occurred 34 | */ 35 | export function useDocument( 36 | reference: DocumentReference | undefined | null, 37 | options?: UseDocumentOptions | undefined, 38 | ): UseDocumentResult { 39 | const { snapshotListenOptions } = options ?? {}; 40 | const { includeMetadataChanges = false, source = "default" } = (snapshotListenOptions ?? 41 | {}) as SnapshotListenOptionsInternal; 42 | 43 | const onChange: UseListenOnChange< 44 | DocumentSnapshot, 45 | FirestoreError, 46 | DocumentReference 47 | > = useCallback( 48 | (stableRef, next, error) => 49 | onSnapshot( 50 | stableRef, 51 | { 52 | includeMetadataChanges, 53 | source, 54 | } as SnapshotListenOptions, 55 | { 56 | next, 57 | error, 58 | }, 59 | ), 60 | [includeMetadataChanges, source], 61 | ); 62 | 63 | return useListen(reference ?? undefined, onChange, isDocRefEqual, LoadingState); 64 | } 65 | -------------------------------------------------------------------------------- /src/firestore/useDocumentData.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DocumentData, 3 | DocumentReference, 4 | FirestoreError, 5 | onSnapshot, 6 | SnapshotListenOptions, 7 | SnapshotOptions, 8 | } from "firebase/firestore"; 9 | import { useCallback } from "react"; 10 | import type { ValueHookResult } from "../common/types.js"; 11 | import { useListen, UseListenOnChange } from "../internal/useListen.js"; 12 | import { LoadingState } from "../internal/useLoadingValue.js"; 13 | import { isDocRefEqual, SnapshotListenOptionsInternal } from "./internal.js"; 14 | 15 | export type UseDocumentDataResult = ValueHookResult; 16 | 17 | /** 18 | * Options to configure the subscription 19 | */ 20 | export interface UseDocumentDataOptions { 21 | snapshotListenOptions?: SnapshotListenOptions | undefined; 22 | snapshotOptions?: SnapshotOptions | undefined; 23 | initialValue?: AppModelType | undefined; 24 | } 25 | 26 | /** 27 | * Returns and updates the data of a Firestore DocumentReference 28 | * @template AppModelType Shape of the data after it was converted from firestore 29 | * @template DbModelType Shape of the data in firestore 30 | * @param reference Firestore DocumentReference that will be subscribed to 31 | * @param options Options to configure the subscription 32 | * `initialValue`: Value that is returned while the document is being fetched. 33 | * @returns Document data, loading state, and error 34 | * - value: Document data; `undefined` if document does not exist, is currently being fetched, or an error occurred 35 | * - loading: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred 36 | * - error: `undefined` if no error occurred 37 | */ 38 | export function useDocumentData( 39 | reference: DocumentReference | undefined | null, 40 | options?: UseDocumentDataOptions | undefined, 41 | ): UseDocumentDataResult { 42 | const { snapshotListenOptions, snapshotOptions } = options ?? {}; 43 | const { includeMetadataChanges = false, source = "default" } = (snapshotListenOptions ?? 44 | {}) as SnapshotListenOptionsInternal; 45 | const { serverTimestamps = "none" } = snapshotOptions ?? {}; 46 | 47 | const onChange: UseListenOnChange< 48 | AppModelType, 49 | FirestoreError, 50 | DocumentReference 51 | > = useCallback( 52 | (stableRef, next, error) => 53 | onSnapshot( 54 | stableRef, 55 | { 56 | includeMetadataChanges, 57 | source, 58 | } as SnapshotListenOptions, 59 | { 60 | next: (snap) => next(snap.data({ serverTimestamps })), 61 | error, 62 | }, 63 | ), 64 | [includeMetadataChanges, serverTimestamps, source], 65 | ); 66 | 67 | return useListen(reference ?? undefined, onChange, isDocRefEqual, options?.initialValue ?? LoadingState); 68 | } 69 | -------------------------------------------------------------------------------- /src/firestore/useDocumentDataOnce.ts: -------------------------------------------------------------------------------- 1 | import { DocumentData, DocumentReference, FirestoreError, SnapshotOptions } from "firebase/firestore"; 2 | import { useCallback } from "react"; 3 | import type { ValueHookResult } from "../common/types.js"; 4 | import { useGet } from "../internal/useGet.js"; 5 | import { getDocFromSource, isDocRefEqual } from "./internal.js"; 6 | import type { Source } from "./types.js"; 7 | 8 | export type UseDocumentDataOnceResult = ValueHookResult; 9 | 10 | /** 11 | * Options to configure how the document is fetched 12 | */ 13 | export interface UseDocumentDataOnceOptions { 14 | source?: Source | undefined; 15 | snapshotOptions?: SnapshotOptions | undefined; 16 | } 17 | 18 | /** 19 | * Returns the data of a Firestore DocumentReference 20 | * @template AppModelType Shape of the data after it was converted from firestore 21 | * @template DbModelType Shape of the data in firestore 22 | * @param reference Firestore DocumentReference that will be subscribed to 23 | * @param options Options to configure how the document is fetched 24 | * @returns Document data, loading state, and error 25 | * - value: Document data; `undefined` if document does not exist, is currently being fetched, or an error occurred 26 | * - loading: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred 27 | * - error: `undefined` if no error occurred 28 | */ 29 | export function useDocumentDataOnce( 30 | reference: DocumentReference | undefined | null, 31 | options?: UseDocumentDataOnceOptions | undefined, 32 | ): UseDocumentDataOnceResult { 33 | const { source = "default", snapshotOptions } = options ?? {}; 34 | const { serverTimestamps = "none" } = snapshotOptions ?? {}; 35 | 36 | const getData = useCallback( 37 | async (stableRef: DocumentReference) => { 38 | const snap = await getDocFromSource(stableRef, source); 39 | return snap.data({ serverTimestamps }); 40 | }, 41 | [serverTimestamps, source], 42 | ); 43 | 44 | return useGet(reference ?? undefined, getData, isDocRefEqual); 45 | } 46 | -------------------------------------------------------------------------------- /src/firestore/useDocumentOnce.ts: -------------------------------------------------------------------------------- 1 | import { DocumentData, DocumentReference, DocumentSnapshot, FirestoreError } from "firebase/firestore"; 2 | import { useCallback } from "react"; 3 | import type { ValueHookResult } from "../common/types.js"; 4 | import { useGet } from "../internal/useGet.js"; 5 | import { getDocFromSource, isDocRefEqual } from "./internal.js"; 6 | import type { Source } from "./types.js"; 7 | 8 | export type UseDocumentOnceResult = ValueHookResult< 9 | DocumentSnapshot, 10 | FirestoreError 11 | >; 12 | 13 | /** 14 | * Options to configure how the document is fetched 15 | */ 16 | export interface UseDocumentOnceOptions { 17 | source?: Source | undefined; 18 | } 19 | 20 | /** 21 | * Returns the DocumentSnapshot of a Firestore DocumentReference. Does not update the DocumentSnapshot once initially fetched 22 | * @template AppModelType Shape of the data after it was converted from firestore 23 | * @template DbModelType Shape of the data in firestore 24 | * @param reference Firestore DocumentReference that will be fetched 25 | * @param options Options to configure how the document is fetched 26 | * @returns DocumentSnapshot, loading state, and error 27 | * - value: DocumentSnapshot; `undefined` if document does not exist, is currently being fetched, or an error occurred 28 | * - loading: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred 29 | * - error: `undefined` if no error occurred 30 | */ 31 | export function useDocumentOnce( 32 | reference: DocumentReference | undefined | null, 33 | options?: UseDocumentOnceOptions | undefined, 34 | ): UseDocumentOnceResult { 35 | const { source = "default" } = options ?? {}; 36 | 37 | const getData = useCallback( 38 | (stableRef: DocumentReference) => getDocFromSource(stableRef, source), 39 | [source], 40 | ); 41 | 42 | return useGet(reference ?? undefined, getData, isDocRefEqual); 43 | } 44 | -------------------------------------------------------------------------------- /src/firestore/useQueries.test-d.ts: -------------------------------------------------------------------------------- 1 | import { Query, QuerySnapshot } from "firebase/firestore"; 2 | import { describe, expectTypeOf, it } from "vitest"; 3 | import { useQueries } from "./useQueries.js"; 4 | 5 | describe("useQueries", () => { 6 | it("single query", () => { 7 | type Value = { key: "value" }; 8 | const query = null as unknown as Query; 9 | 10 | const results = useQueries([query]); 11 | 12 | expectTypeOf<(typeof results)[0][0]>().toMatchTypeOf | undefined>(); 13 | }); 14 | 15 | it("multiple queries", () => { 16 | type Value1 = { key: "value" }; 17 | type Value2 = { key: "value2" }; 18 | const query1 = null as unknown as Query; 19 | const query2 = null as unknown as Query; 20 | 21 | const results = useQueries([query1, query2] as const); 22 | 23 | expectTypeOf<(typeof results)[0][0]>().toMatchTypeOf | undefined>(); 24 | expectTypeOf<(typeof results)[1][0]>().toMatchTypeOf | undefined>(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/firestore/useQueries.ts: -------------------------------------------------------------------------------- 1 | import { DocumentData, FirestoreError, onSnapshot, Query, QuerySnapshot, SnapshotListenOptions } from "firebase/firestore"; 2 | import { useCallback } from "react"; 3 | import { ValueHookResult } from "../common/types.js"; 4 | import { useMultiListen, UseMultiListenChange } from "../internal/useMultiListen.js"; 5 | import { isQueryEqual, SnapshotListenOptionsInternal } from "./internal.js"; 6 | 7 | export type UseQueriesResult = ReadonlyArray> = { 8 | [Index in keyof AppModelTypes]: ValueHookResult, FirestoreError>; 9 | } & { length: AppModelTypes["length"] }; 10 | 11 | /** 12 | * Options to configure the subscription 13 | */ 14 | export interface UseQueriesOptions { 15 | snapshotListenOptions?: SnapshotListenOptions | undefined; 16 | } 17 | 18 | /** 19 | * Returns and updates a QuerySnapshot of multiple Firestore queries 20 | * @template AppModelTypes Tuple of shapes of the data after it was converted from firestore 21 | * @template DbModelTypes Tuple of shapes of the data in firestore 22 | * @param queries Firestore queries that will be subscribed to 23 | * @param options Options to configure the subscription 24 | * @returns Array with tuple for each query: 25 | * - value: QuerySnapshot; `undefined` if query is currently being fetched, or an error occurred 26 | * - loading: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 27 | * - error: `undefined` if no error occurred 28 | */ 29 | export function useQueries< 30 | AppModelTypes extends ReadonlyArray = ReadonlyArray, 31 | DbModelTypes extends ReadonlyArray = ReadonlyArray, 32 | >( 33 | queries: { [Index in keyof AppModelTypes]: Query }, 34 | options?: UseQueriesOptions | undefined, 35 | ): UseQueriesResult { 36 | const { snapshotListenOptions } = options ?? {}; 37 | const { includeMetadataChanges = false, source = "default" } = (snapshotListenOptions ?? 38 | {}) as SnapshotListenOptionsInternal; 39 | 40 | const onChange: UseMultiListenChange< 41 | QuerySnapshot, 42 | FirestoreError, 43 | Query 44 | > = useCallback( 45 | (query, next, error) => 46 | onSnapshot( 47 | query, 48 | { 49 | includeMetadataChanges, 50 | source, 51 | } as SnapshotListenOptions, 52 | { 53 | next, 54 | error, 55 | }, 56 | ), 57 | [includeMetadataChanges, source], 58 | ); 59 | 60 | // @ts-expect-error `useMultiListen` assumes a single value type 61 | return useMultiListen(queries, onChange, isQueryEqual); 62 | } 63 | -------------------------------------------------------------------------------- /src/firestore/useQueriesData.test-d.ts: -------------------------------------------------------------------------------- 1 | import { Query } from "firebase/firestore"; 2 | import { describe, expectTypeOf, it } from "vitest"; 3 | import { useQueriesData } from "./useQueriesData.js"; 4 | 5 | describe("useQueriesData", () => { 6 | it("single query", () => { 7 | type Value = { key: "value" }; 8 | const query = null as unknown as Query; 9 | 10 | const results = useQueriesData([query]); 11 | 12 | expectTypeOf<(typeof results)[0][0]>().toMatchTypeOf(); 13 | }); 14 | 15 | it("multiple queries", () => { 16 | type Value1 = { key: "value" }; 17 | type Value2 = { key: "value2" }; 18 | const query1 = null as unknown as Query; 19 | const query2 = null as unknown as Query; 20 | 21 | const results = useQueriesData([query1, query2] as const); 22 | 23 | expectTypeOf<(typeof results)[0][0]>().toMatchTypeOf(); 24 | expectTypeOf<(typeof results)[1][0]>().toMatchTypeOf(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/firestore/useQueriesData.ts: -------------------------------------------------------------------------------- 1 | import { DocumentData, FirestoreError, onSnapshot, Query, SnapshotListenOptions, SnapshotOptions } from "firebase/firestore"; 2 | import { useCallback } from "react"; 3 | import { ValueHookResult } from "../common/types.js"; 4 | import { useMultiListen, UseMultiListenChange } from "../internal/useMultiListen.js"; 5 | import { isQueryEqual, SnapshotListenOptionsInternal } from "./internal.js"; 6 | 7 | export type UseQueriesDataResult = ReadonlyArray> = { 8 | [Index in keyof AppModelTypes]: ValueHookResult; 9 | } & { length: AppModelTypes["length"] }; 10 | 11 | /** 12 | * Options to configure the subscription 13 | */ 14 | export interface UseQueriesDataOptions { 15 | snapshotListenOptions?: SnapshotListenOptions | undefined; 16 | snapshotOptions?: SnapshotOptions | undefined; 17 | } 18 | 19 | /** 20 | * Returns and updates a the document data of multiple Firestore queries 21 | * @template AppModelTypes Tuple of shapes of the data after it was converted from firestore 22 | * @template DbModelTypes Tuple of shapes of the data in firestore 23 | * @param queries Firestore queries that will be subscribed to 24 | * @param options Options to configure the subscription 25 | * @returns Array with tuple for each query: 26 | * - value: Query data; `undefined` if query is currently being fetched, or an error occurred 27 | * - loading: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 28 | * - error: `undefined` if no error occurred 29 | */ 30 | export function useQueriesData< 31 | AppModelTypes extends ReadonlyArray = ReadonlyArray, 32 | DbModelTypes extends ReadonlyArray = ReadonlyArray, 33 | >( 34 | queries: { [Index in keyof AppModelTypes]: Query }, 35 | options?: UseQueriesDataOptions | undefined, 36 | ): UseQueriesDataResult { 37 | const { snapshotListenOptions, snapshotOptions } = options ?? {}; 38 | const { includeMetadataChanges = false, source = "default" } = (snapshotListenOptions ?? 39 | {}) as SnapshotListenOptionsInternal; 40 | const { serverTimestamps = "none" } = snapshotOptions ?? {}; 41 | 42 | const onChange: UseMultiListenChange< 43 | AppModelTypes[number], 44 | FirestoreError, 45 | Query 46 | > = useCallback( 47 | (query, next, error) => 48 | onSnapshot( 49 | query, 50 | { 51 | includeMetadataChanges, 52 | source, 53 | } as SnapshotListenOptions, 54 | { 55 | next: (snap) => next(snap.docs.map((doc) => doc.data({ serverTimestamps }))), 56 | error, 57 | }, 58 | ), 59 | [includeMetadataChanges, serverTimestamps, source], 60 | ); 61 | 62 | // @ts-expect-error `useMultiListen` assumes a single value type 63 | return useMultiListen(queries, onChange, isQueryEqual); 64 | } 65 | -------------------------------------------------------------------------------- /src/firestore/useQueriesDataOnce.ts: -------------------------------------------------------------------------------- 1 | import { DocumentData, FirestoreError, Query, SnapshotOptions } from "firebase/firestore"; 2 | import { useCallback } from "react"; 3 | import type { ValueHookResult } from "../common/types.js"; 4 | import { useMultiGet } from "../internal/useMultiGet.js"; 5 | import { getDocsFromSource, isQueryEqual } from "./internal.js"; 6 | import type { Source } from "./types.js"; 7 | 8 | export type UseQueriesDataOnceResult = ReadonlyArray> = { 9 | [Index in keyof AppModelTypes]: ValueHookResult; 10 | } & { length: AppModelTypes["length"] }; 11 | 12 | /** 13 | * Options to configure the subscription 14 | */ 15 | export interface UseQueriesDataOnceOptions { 16 | source?: Source | undefined; 17 | snapshotOptions?: SnapshotOptions | undefined; 18 | } 19 | 20 | /** 21 | * Returns the data of multiple Firestore queries. Does not update the data once initially fetched 22 | * @template AppModelTypes Tuple of shapes of the data after it was converted from firestore 23 | * @template DbModelTypes Tuple of shapes of the data in firestore 24 | * @param queries Firestore queries that will be fetched 25 | * @param options Options to configure how the queries are fetched 26 | * @returns Array with tuple for each query: 27 | * - value: Query data; `undefined` if query is currently being fetched, or an error occurred 28 | * - loading: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 29 | * - error: `undefined` if no error occurred 30 | */ 31 | export function useQueriesDataOnce< 32 | AppModelTypes extends ReadonlyArray = ReadonlyArray, 33 | DbModelTypes extends ReadonlyArray = ReadonlyArray, 34 | >( 35 | queries: { [Index in keyof AppModelTypes]: Query }, 36 | options?: UseQueriesDataOnceOptions | undefined, 37 | ): UseQueriesDataOnceResult { 38 | const { source = "default", snapshotOptions } = options ?? {}; 39 | const { serverTimestamps = "none" } = snapshotOptions ?? {}; 40 | 41 | const getData = useCallback( 42 | async (stableQuery: Query) => { 43 | const snap = await getDocsFromSource(stableQuery, source); 44 | return snap.docs.map((doc) => doc.data({ serverTimestamps })); 45 | }, 46 | [source, serverTimestamps], 47 | ); 48 | 49 | // @ts-expect-error `useMultiGet` assumes a single value type 50 | return useMultiGet(queries, getData, isQueryEqual); 51 | } 52 | -------------------------------------------------------------------------------- /src/firestore/useQueriesOnce.ts: -------------------------------------------------------------------------------- 1 | import { DocumentData, FirestoreError, Query, SnapshotOptions } from "firebase/firestore"; 2 | import { useCallback } from "react"; 3 | import type { ValueHookResult } from "../common/types.js"; 4 | import { useMultiGet } from "../internal/useMultiGet.js"; 5 | import { getDocsFromSource, isQueryEqual } from "./internal.js"; 6 | import type { Source } from "./types.js"; 7 | 8 | export type UseQueriesOnceResult = ReadonlyArray> = { 9 | [Index in keyof AppModelTypes]: ValueHookResult; 10 | } & { length: AppModelTypes["length"] }; 11 | 12 | /** 13 | * Options to configure the subscription 14 | */ 15 | export interface UseQueriesOnceOptions { 16 | source?: Source | undefined; 17 | snapshotOptions?: SnapshotOptions | undefined; 18 | } 19 | 20 | /** 21 | * Returns the QuerySnapshot of multiple Firestore queries. Does not update the data once initially fetched 22 | * @template AppModelTypes Tuple of shapes of the data after it was converted from firestore 23 | * @template DbModelTypes Tuple of shapes of the data in firestore 24 | * @param queries Firestore queries that will be fetched 25 | * @param options Options to configure how the queries are fetched 26 | * @returns Array with tuple for each query: 27 | * - value: QuerySnapshot; `undefined` if query is currently being fetched, or an error occurred 28 | * - loading: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 29 | * - error: `undefined` if no error occurred 30 | */ 31 | export function useQueriesOnce< 32 | AppModelTypes extends ReadonlyArray = ReadonlyArray, 33 | DbModelTypes extends ReadonlyArray = ReadonlyArray, 34 | >( 35 | queries: { [Index in keyof AppModelTypes]: Query }, 36 | options?: UseQueriesOnceOptions | undefined, 37 | ): UseQueriesOnceResult { 38 | const { source = "default" } = options ?? {}; 39 | 40 | const getData = useCallback( 41 | async (stableQuery: Query) => getDocsFromSource(stableQuery, source), 42 | [source], 43 | ); 44 | 45 | // @ts-expect-error `useMultiGet` assumes a single value type 46 | return useMultiGet(queries, getData, isQueryEqual); 47 | } 48 | -------------------------------------------------------------------------------- /src/firestore/useQuery.ts: -------------------------------------------------------------------------------- 1 | import { DocumentData, FirestoreError, onSnapshot, Query, QuerySnapshot, SnapshotListenOptions } from "firebase/firestore"; 2 | import { useCallback } from "react"; 3 | import { ValueHookResult } from "../common/types.js"; 4 | import { useListen, UseListenOnChange } from "../internal/useListen.js"; 5 | import { LoadingState } from "../internal/useLoadingValue.js"; 6 | import { isQueryEqual, SnapshotListenOptionsInternal } from "./internal.js"; 7 | 8 | export type UseQueryResult = ValueHookResult, FirestoreError>; 9 | 10 | /** 11 | * Options to configure the subscription 12 | */ 13 | export interface UseQueryOptions { 14 | snapshotListenOptions?: SnapshotListenOptions | undefined; 15 | } 16 | 17 | /** 18 | * Returns and updates a QuerySnapshot of a Firestore Query 19 | * @template AppModelType Shape of the data after it was converted from firestore 20 | * @template DbModelType Shape of the data in firestore 21 | * @param query Firestore query that will be subscribed to 22 | * @param options Options to configure the subscription 23 | * @returns QuerySnapshot, loading, and error 24 | * - value: QuerySnapshot; `undefined` if query is currently being fetched, or an error occurred 25 | * - loading: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 26 | * - error: `undefined` if no error occurred 27 | */ 28 | export function useQuery( 29 | query: Query | undefined | null, 30 | options?: UseQueryOptions | undefined, 31 | ): UseQueryResult { 32 | const { snapshotListenOptions } = options ?? {}; 33 | const { includeMetadataChanges = false, source = "default" } = (snapshotListenOptions ?? 34 | {}) as SnapshotListenOptionsInternal; 35 | 36 | const onChange: UseListenOnChange< 37 | QuerySnapshot, 38 | FirestoreError, 39 | Query 40 | > = useCallback( 41 | (stableQuery, next, error) => 42 | onSnapshot( 43 | stableQuery, 44 | { 45 | includeMetadataChanges, 46 | source, 47 | } as SnapshotListenOptions, 48 | { 49 | next, 50 | error, 51 | }, 52 | ), 53 | [includeMetadataChanges, source], 54 | ); 55 | 56 | return useListen(query ?? undefined, onChange, isQueryEqual, LoadingState); 57 | } 58 | -------------------------------------------------------------------------------- /src/firestore/useQueryData.ts: -------------------------------------------------------------------------------- 1 | import { DocumentData, FirestoreError, onSnapshot, Query, SnapshotListenOptions, SnapshotOptions } from "firebase/firestore"; 2 | import { useCallback } from "react"; 3 | import { ValueHookResult } from "../common/types.js"; 4 | import { useListen, UseListenOnChange } from "../internal/useListen.js"; 5 | import { LoadingState } from "../internal/useLoadingValue.js"; 6 | import { isQueryEqual, SnapshotListenOptionsInternal } from "./internal.js"; 7 | 8 | export type UseQueryDataResult = ValueHookResult; 9 | 10 | /** 11 | * Options to configure the subscription 12 | */ 13 | export interface UseQueryDataOptions { 14 | snapshotListenOptions?: SnapshotListenOptions | undefined; 15 | snapshotOptions?: SnapshotOptions | undefined; 16 | initialValue?: AppModelType[] | undefined; 17 | } 18 | 19 | /** 20 | * Returns and updates a the document data of a Firestore Query 21 | * @template AppModelType Shape of the data after it was converted from firestore 22 | * @template DbModelType Shape of the data in firestore 23 | * @param query Firestore query that will be subscribed to 24 | * @param options Options to configure the subscription 25 | * `initialValue`: Value that is returned while the query is being fetched. 26 | * @returns Query data, loading state, and error 27 | * - value: Query data; `undefined` if query is currently being fetched, or an error occurred 28 | * - loading: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 29 | * - error: `undefined` if no error occurred 30 | */ 31 | export function useQueryData( 32 | query: Query | undefined | null, 33 | options?: UseQueryDataOptions | undefined, 34 | ): UseQueryDataResult { 35 | const { snapshotListenOptions, snapshotOptions } = options ?? {}; 36 | const { includeMetadataChanges = false, source = "default" } = (snapshotListenOptions ?? 37 | {}) as SnapshotListenOptionsInternal; 38 | const { serverTimestamps = "none" } = snapshotOptions ?? {}; 39 | 40 | const onChange: UseListenOnChange> = useCallback( 41 | (stableQuery, next, error) => 42 | onSnapshot( 43 | stableQuery, 44 | { 45 | includeMetadataChanges, 46 | source, 47 | } as SnapshotListenOptions, 48 | { 49 | next: (snap) => next(snap.docs.map((doc) => doc.data({ serverTimestamps }))), 50 | error, 51 | }, 52 | ), 53 | [includeMetadataChanges, serverTimestamps, source], 54 | ); 55 | 56 | return useListen(query ?? undefined, onChange, isQueryEqual, options?.initialValue ?? LoadingState); 57 | } 58 | -------------------------------------------------------------------------------- /src/firestore/useQueryDataOnce.ts: -------------------------------------------------------------------------------- 1 | import { DocumentData, FirestoreError, Query, SnapshotOptions } from "firebase/firestore"; 2 | import { useCallback } from "react"; 3 | import type { ValueHookResult } from "../common/types.js"; 4 | import { useGet } from "../internal/useGet.js"; 5 | import { getDocsFromSource, isQueryEqual } from "./internal.js"; 6 | import type { Source } from "./types.js"; 7 | 8 | export type UseQueryDataOnceResult = ValueHookResult; 9 | 10 | /** 11 | * Options to configure the subscription 12 | */ 13 | export interface UseQueryDataOnceOptions { 14 | source?: Source | undefined; 15 | snapshotOptions?: SnapshotOptions | undefined; 16 | } 17 | 18 | /** 19 | * Returns the data of a Firestore Query. Does not update the data once initially fetched 20 | * @template AppModelType Shape of the data after it was converted from firestore 21 | * @template DbModelType Shape of the data in firestore 22 | * @param query Firestore query that will be fetched 23 | * @param options Options to configure how the query is fetched 24 | * @returns Query data, loading state, and error 25 | * - value: Query data; `undefined` if query is currently being fetched, or an error occurred 26 | * - loading: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 27 | * - error: `undefined` if no error occurred 28 | */ 29 | export function useQueryDataOnce( 30 | query: Query | undefined | null, 31 | options?: UseQueryDataOnceOptions | undefined, 32 | ): UseQueryDataOnceResult { 33 | const { source = "default", snapshotOptions } = options ?? {}; 34 | const { serverTimestamps = "none" } = snapshotOptions ?? {}; 35 | 36 | const getData = useCallback( 37 | async (stableQuery: Query) => { 38 | const snap = await getDocsFromSource(stableQuery, source); 39 | return snap.docs.map((doc) => doc.data({ serverTimestamps })); 40 | }, 41 | [serverTimestamps, source], 42 | ); 43 | 44 | return useGet(query ?? undefined, getData, isQueryEqual); 45 | } 46 | -------------------------------------------------------------------------------- /src/firestore/useQueryOnce.ts: -------------------------------------------------------------------------------- 1 | import { DocumentData, FirestoreError, Query, QuerySnapshot } from "firebase/firestore"; 2 | import { useCallback } from "react"; 3 | import type { ValueHookResult } from "../common/types.js"; 4 | import { useGet } from "../internal/useGet.js"; 5 | import { getDocsFromSource, isQueryEqual } from "./internal.js"; 6 | import type { Source } from "./types.js"; 7 | 8 | export type UseQueryOnceResult = ValueHookResult, FirestoreError>; 9 | 10 | /** 11 | * Options to configure how the query is fetched 12 | */ 13 | export interface UseQueryOnceOptions { 14 | source?: Source | undefined; 15 | } 16 | 17 | /** 18 | * Returns the QuerySnapshot of a Firestore query. Does not update the QuerySnapshot once initially fetched 19 | * @template AppModelType Shape of the data after it was converted from firestore 20 | * @template DbModelType Shape of the data in firestore 21 | * @param query Firestore query that will be fetched 22 | * @param options Options to configure how the query is fetched 23 | * @returns QuerySnapshot, loading state, and error 24 | * - value: QuerySnapshot; `undefined` if query is currently being fetched, or an error occurred 25 | * - loading: `true` while fetching the query; `false` if the query was fetched successfully or an error occurred 26 | * - error: `undefined` if no error occurred 27 | */ 28 | export function useQueryOnce( 29 | query: Query | undefined | null, 30 | options?: UseQueryOnceOptions | undefined, 31 | ): UseQueryOnceResult { 32 | const { source = "default" } = options ?? {}; 33 | 34 | const getData = useCallback( 35 | async (stableQuery: Query) => getDocsFromSource(stableQuery, source), 36 | [source], 37 | ); 38 | 39 | return useGet(query ?? undefined, getData, isQueryEqual); 40 | } 41 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./app-check/index.js"; 2 | export * from "./auth/index.js"; 3 | export * from "./common/index.js"; 4 | export * from "./database/index.js"; 5 | export * from "./firestore/index.js"; 6 | export * from "./messaging/index.js"; 7 | export * from "./storage/index.js"; 8 | -------------------------------------------------------------------------------- /src/internal/useGet.spec.ts: -------------------------------------------------------------------------------- 1 | import { configure, renderHook, waitFor } from "@testing-library/react"; 2 | import { newPromise, newSymbol } from "../__testfixtures__/index.js"; 3 | import { useGet } from "./useGet.js"; 4 | import { it, expect, beforeEach, describe, vi, afterEach } from "vitest"; 5 | 6 | const result1 = newSymbol("Result 1"); 7 | const result2 = newSymbol("Result 2"); 8 | const error1 = newSymbol("Error 1"); 9 | const error2 = newSymbol("Error 2"); 10 | 11 | const refA1 = newSymbol("Ref A1"); 12 | const refA2 = newSymbol("Ref A2"); 13 | 14 | const refB1 = newSymbol("Ref B1"); 15 | const refB2 = newSymbol("Ref B2"); 16 | 17 | const isEqual = (a: unknown, b: unknown) => 18 | [a, b].every((x) => [refA1, refA2].includes(x)) || [a, b].every((x) => [refB1, refB2].includes(x)); 19 | 20 | afterEach(() => { 21 | configure({ reactStrictMode: false }); 22 | }); 23 | 24 | describe.each([{ reactStrictMode: true }, { reactStrictMode: false }])( 25 | `strictMode=$reactStrictMode`, 26 | ({ reactStrictMode }) => { 27 | beforeEach(() => { 28 | configure({ reactStrictMode }); 29 | }); 30 | 31 | describe("initial state", () => { 32 | it("defined reference", () => { 33 | const getData = vi.fn().mockReturnValue(new Promise(() => {})); 34 | const { result } = renderHook(() => useGet(refA1, getData, isEqual)); 35 | expect(result.current).toStrictEqual([undefined, true, undefined]); 36 | }); 37 | 38 | it("undefined reference", () => { 39 | const getData = vi.fn(); 40 | const { result } = renderHook(() => useGet(undefined, getData, isEqual)); 41 | expect(result.current).toStrictEqual([undefined, false, undefined]); 42 | }); 43 | }); 44 | }, 45 | ); 46 | 47 | describe("initial load", () => { 48 | it("should return success result", async () => { 49 | const { promise, resolve } = newPromise(); 50 | const getData = vi.fn().mockReturnValue(promise); 51 | 52 | const { result } = renderHook(() => useGet(refA1, getData, isEqual)); 53 | 54 | // initial state 55 | expect(result.current).toStrictEqual([undefined, true, undefined]); 56 | 57 | // resolve promise 58 | resolve(result1); 59 | await waitFor(() => expect(result.current).toStrictEqual([result1, false, undefined])); 60 | }); 61 | 62 | it("should return error result", async () => { 63 | const { promise, reject } = newPromise(); 64 | const getData = vi.fn().mockReturnValue(promise); 65 | 66 | const { result } = renderHook(() => useGet(refA1, getData, isEqual)); 67 | 68 | // initial state 69 | expect(result.current).toStrictEqual([undefined, true, undefined]); 70 | 71 | // reject promise 72 | reject(error1); 73 | await waitFor(() => expect(result.current).toStrictEqual([undefined, false, error1])); 74 | }); 75 | }); 76 | 77 | describe("when ref changes", () => { 78 | describe("to equal ref", () => { 79 | it("should not update success result", async () => { 80 | const getData = vi.fn().mockResolvedValueOnce(result1); 81 | 82 | const { result, rerender } = renderHook(({ ref }) => useGet(ref, getData, isEqual), { 83 | initialProps: { ref: refA1 }, 84 | }); 85 | 86 | // initial state 87 | expect(result.current).toStrictEqual([undefined, true, undefined]); 88 | await waitFor(() => expect(result.current).toStrictEqual([result1, false, undefined])); 89 | expect(getData).toHaveBeenCalledTimes(1); 90 | 91 | // change ref 92 | rerender({ ref: refA2 }); 93 | expect(result.current).toStrictEqual([result1, false, undefined]); 94 | expect(getData).toHaveBeenCalledTimes(1); 95 | }); 96 | 97 | it("should not update error result", async () => { 98 | const getData = vi.fn().mockRejectedValueOnce(error1); 99 | 100 | const { result, rerender } = renderHook(({ ref }) => useGet(ref, getData, isEqual), { 101 | initialProps: { ref: refA1 }, 102 | }); 103 | 104 | // initial state 105 | expect(result.current).toStrictEqual([undefined, true, undefined]); 106 | await waitFor(() => expect(result.current).toStrictEqual([undefined, false, error1])); 107 | expect(getData).toHaveBeenCalledTimes(1); 108 | 109 | // change ref 110 | rerender({ ref: refA2 }); 111 | expect(result.current).toStrictEqual([undefined, false, error1]); 112 | expect(getData).toHaveBeenCalledTimes(1); 113 | }); 114 | }); 115 | 116 | describe("to unequal ref", () => { 117 | it("should update success result", async () => { 118 | const getData = vi.fn().mockResolvedValueOnce(result1).mockResolvedValueOnce(result2); 119 | 120 | const { result, rerender } = renderHook(({ ref }) => useGet(ref, getData, isEqual), { 121 | initialProps: { ref: refA1 }, 122 | }); 123 | 124 | // initial state 125 | expect(result.current).toStrictEqual([undefined, true, undefined]); 126 | expect(getData).toHaveBeenCalledTimes(1); 127 | await waitFor(() => expect(result.current).toStrictEqual([result1, false, undefined])); 128 | 129 | // change ref 130 | rerender({ ref: refB1 }); 131 | expect(getData).toHaveBeenCalledTimes(2); 132 | await waitFor(() => expect(result.current).toStrictEqual([result2, false, undefined])); 133 | }); 134 | 135 | it("should update error result", async () => { 136 | const getData = vi.fn().mockRejectedValueOnce(error1).mockResolvedValueOnce(result2); 137 | 138 | const { result, rerender } = renderHook(({ ref }) => useGet(ref, getData, isEqual), { 139 | initialProps: { ref: refA1 }, 140 | }); 141 | 142 | // initial state 143 | expect(result.current).toStrictEqual([undefined, true, undefined]); 144 | expect(getData).toHaveBeenCalledTimes(1); 145 | await waitFor(() => expect(result.current).toStrictEqual([undefined, false, error1])); 146 | 147 | // change ref 148 | rerender({ ref: refB1 }); 149 | expect(result.current).toStrictEqual([undefined, true, undefined]); 150 | expect(getData).toHaveBeenCalledTimes(2); 151 | await waitFor(() => expect(result.current).toStrictEqual([result2, false, undefined])); 152 | }); 153 | 154 | describe("if changed before first `getData` is settled", () => { 155 | it("should ignore the first result", async () => { 156 | const { promise: promise1, resolve: resolve1 } = newPromise(); 157 | const { promise: promise2, resolve: resolve2 } = newPromise(); 158 | const getData = vi.fn().mockReturnValueOnce(promise1).mockReturnValueOnce(promise2); 159 | 160 | const { result, rerender } = renderHook(({ ref }) => useGet(ref, getData, isEqual), { 161 | initialProps: { ref: refA1 }, 162 | }); 163 | 164 | // initial state 165 | await waitFor(() => expect(result.current).toStrictEqual([undefined, true, undefined])); 166 | 167 | // change ref 168 | rerender({ ref: refB1 }); 169 | await waitFor(() => expect(result.current).toStrictEqual([undefined, true, undefined])); 170 | 171 | // first promise resolves 172 | resolve1(result1); 173 | 174 | // ensure that the first result is ignored 175 | await expect( 176 | waitFor( 177 | () => { 178 | expect(result.current).toStrictEqual([result1, false, undefined]); 179 | }, 180 | { timeout: 200 }, 181 | ), 182 | ).rejects.toThrow(); 183 | 184 | // second promise resolves 185 | resolve2(result2); 186 | await waitFor(() => expect(result.current).toStrictEqual([result2, false, undefined])); 187 | }); 188 | 189 | it("should ignore the first thrown error", async () => { 190 | const { promise: promise1, reject: reject1 } = newPromise(); 191 | const { promise: promise2, reject: reject2 } = newPromise(); 192 | const getData = vi.fn().mockReturnValueOnce(promise1).mockReturnValueOnce(promise2); 193 | 194 | const { result, rerender } = renderHook(({ ref }) => useGet(ref, getData, isEqual), { 195 | initialProps: { ref: refA1 }, 196 | }); 197 | 198 | // initial state 199 | await waitFor(() => expect(result.current).toStrictEqual([undefined, true, undefined])); 200 | 201 | // change ref 202 | rerender({ ref: refB1 }); 203 | await waitFor(() => expect(result.current).toStrictEqual([undefined, true, undefined])); 204 | 205 | // first promise rejects 206 | reject1(error1); 207 | 208 | // ensure that the first result is ignored 209 | await expect( 210 | waitFor( 211 | () => { 212 | expect(result.current).toStrictEqual([undefined, false, error1]); 213 | }, 214 | { timeout: 200 }, 215 | ), 216 | ).rejects.toThrow(); 217 | 218 | // second promise rejects 219 | reject2(error2); 220 | await waitFor(() => expect(result.current).toStrictEqual([undefined, false, error2])); 221 | }); 222 | }); 223 | }); 224 | }); 225 | 226 | it("refetches if `getData` changes", async () => { 227 | const getData1 = vi.fn().mockResolvedValue(result1); 228 | const getData2 = vi.fn().mockResolvedValue(result2); 229 | 230 | const { result, rerender } = renderHook(({ getData }) => useGet(refA1, getData, isEqual), { 231 | initialProps: { getData: getData1 }, 232 | }); 233 | 234 | // initial state 235 | await waitFor(() => { 236 | expect(result.current).toStrictEqual([result1, false, undefined]); 237 | }); 238 | expect(getData1).toHaveBeenCalledTimes(1); 239 | 240 | // changing `getData` 241 | rerender({ getData: getData2 }); 242 | await waitFor(() => { 243 | expect(result.current).toStrictEqual([result2, false, undefined]); 244 | }); 245 | expect(getData2).toHaveBeenCalledTimes(1); 246 | }); 247 | -------------------------------------------------------------------------------- /src/internal/useGet.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useRef } from "react"; 2 | import { ValueHookResult } from "../common/index.js"; 3 | import { useIsMounted } from "./useIsMounted.js"; 4 | import { LoadingState, useLoadingValue } from "./useLoadingValue.js"; 5 | import { useStableValue } from "./useStableValue.js"; 6 | 7 | /** 8 | * @internal 9 | */ 10 | export function useGet( 11 | reference: Reference | undefined, 12 | getData: (ref: Reference) => Promise, 13 | isEqual: (a: Reference | undefined, b: Reference | undefined) => boolean, 14 | ): ValueHookResult { 15 | const isMounted = useIsMounted(); 16 | const { value, setValue, loading, setLoading, error, setError } = useLoadingValue( 17 | reference === undefined ? undefined : LoadingState, 18 | ); 19 | 20 | const stableRef = useStableValue(reference ?? undefined, isEqual); 21 | const ongoingFetchRef = useRef(); 22 | 23 | useEffect(() => { 24 | if (stableRef === undefined) { 25 | setValue(); 26 | } else { 27 | setLoading(); 28 | ongoingFetchRef.current = stableRef; 29 | 30 | (async () => { 31 | try { 32 | const data = await getData(stableRef); 33 | 34 | if (!isMounted.current) { 35 | return; 36 | } 37 | 38 | if (!isEqual(ongoingFetchRef.current, stableRef)) { 39 | return; 40 | } 41 | 42 | setValue(data); 43 | } catch (e) { 44 | if (!isMounted.current) { 45 | return; 46 | } 47 | 48 | if (!isEqual(ongoingFetchRef.current, stableRef)) { 49 | return; 50 | } 51 | 52 | // We assume this is always a Error 53 | setError(e as Error); 54 | } 55 | })(); 56 | } 57 | }, [stableRef, getData, isEqual, setValue, setLoading, setError, isMounted]); 58 | 59 | return useMemo(() => [value, loading, error], [value, loading, error]); 60 | } 61 | -------------------------------------------------------------------------------- /src/internal/useIsMounted.spec.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from "@testing-library/react"; 2 | import { expect, it } from "vitest"; 3 | import { useIsMounted } from "./useIsMounted.js"; 4 | 5 | it("should return `true` on first render", () => { 6 | const { result } = renderHook(() => useIsMounted()); 7 | 8 | expect(result.current.current).toBe(true); 9 | }); 10 | 11 | it("should return `true` after rerender", () => { 12 | const { result, rerender } = renderHook(() => useIsMounted()); 13 | 14 | rerender(); 15 | 16 | expect(result.current.current).toBe(true); 17 | }); 18 | 19 | it("should return `false` after unmount", () => { 20 | const { result, unmount } = renderHook(() => useIsMounted()); 21 | 22 | unmount(); 23 | 24 | expect(result.current.current).toBe(false); 25 | }); 26 | -------------------------------------------------------------------------------- /src/internal/useIsMounted.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | /** 4 | * @internal 5 | */ 6 | export function useIsMounted() { 7 | const isMounted = useRef(false); 8 | 9 | useEffect(() => { 10 | isMounted.current = true; 11 | 12 | return () => { 13 | isMounted.current = false; 14 | }; 15 | }, []); 16 | 17 | return isMounted; 18 | } 19 | -------------------------------------------------------------------------------- /src/internal/useListen.spec.ts: -------------------------------------------------------------------------------- 1 | import { act, configure, renderHook } from "@testing-library/react"; 2 | import { newSymbol } from "../__testfixtures__/index.js"; 3 | import { useListen } from "./useListen.js"; 4 | import { LoadingState } from "./useLoadingValue.js"; 5 | import { it, expect, beforeEach, describe, vi, afterEach } from "vitest"; 6 | 7 | const result1 = newSymbol("Result 1"); 8 | const result2 = newSymbol("Result 2"); 9 | const error = newSymbol("Error"); 10 | 11 | const refA1 = newSymbol("Ref A1"); 12 | const refA2 = newSymbol("Ref A2"); 13 | 14 | const refB1 = newSymbol("Ref B1"); 15 | const refB2 = newSymbol("Ref B2"); 16 | 17 | const onChangeUnsubscribe = vi.fn(); 18 | const onChange = vi.fn(); 19 | 20 | const isEqual = (a: unknown, b: unknown) => 21 | [a, b].every((x) => [refA1, refA2].includes(x)) || [a, b].every((x) => [refB1, refB2].includes(x)); 22 | 23 | beforeEach(() => { 24 | vi.resetAllMocks(); 25 | onChange.mockReturnValue(onChangeUnsubscribe); 26 | }); 27 | 28 | afterEach(() => { 29 | configure({ reactStrictMode: false }); 30 | }); 31 | 32 | describe.each([{ reactStrictMode: true }, { reactStrictMode: false }])( 33 | `strictMode=$reactStrictMode`, 34 | ({ reactStrictMode }) => { 35 | beforeEach(() => { 36 | configure({ reactStrictMode }); 37 | }); 38 | 39 | describe("initial state", () => { 40 | it.each` 41 | reference | initialState | expectedValue | expectedLoading 42 | ${undefined} | ${result1} | ${undefined} | ${false} 43 | ${undefined} | ${undefined} | ${undefined} | ${false} 44 | ${undefined} | ${LoadingState} | ${undefined} | ${false} 45 | ${refA1} | ${result1} | ${result1} | ${false} 46 | ${refA1} | ${undefined} | ${undefined} | ${false} 47 | ${refA1} | ${LoadingState} | ${undefined} | ${true} 48 | `( 49 | "reference=$reference initialState=$initialState", 50 | ({ reference, initialState, expectedValue, expectedLoading }) => { 51 | const { result } = renderHook(() => useListen(reference, onChange, isEqual, initialState)); 52 | expect(result.current).toStrictEqual([expectedValue, expectedLoading, undefined]); 53 | }, 54 | ); 55 | }); 56 | }, 57 | ); 58 | 59 | describe("when changing ref", () => { 60 | it("should not resubscribe for equal ref", async () => { 61 | // first ref 62 | const { result, rerender } = renderHook(({ ref }) => useListen(ref, onChange, isEqual, LoadingState), { 63 | initialProps: { ref: refA1 }, 64 | }); 65 | expect(onChangeUnsubscribe).toHaveBeenCalledTimes(0); 66 | expect(onChange).toHaveBeenCalledTimes(1); 67 | 68 | // emit value 69 | act(() => onChange.mock.calls[0]![1](result1)); 70 | expect(result.current).toStrictEqual([result1, false, undefined]); 71 | 72 | // change ref 73 | rerender({ ref: refA2 }); 74 | expect(result.current).toStrictEqual([result1, false, undefined]); 75 | expect(onChangeUnsubscribe).toHaveBeenCalledTimes(0); 76 | expect(onChange).toHaveBeenCalledTimes(1); 77 | }); 78 | 79 | it("should resubscribe for different ref", () => { 80 | // first ref 81 | const { result, rerender } = renderHook(({ ref }) => useListen(ref, onChange, isEqual, LoadingState), { 82 | initialProps: { ref: refA1 }, 83 | }); 84 | expect(onChangeUnsubscribe).toHaveBeenCalledTimes(0); 85 | expect(onChange).toHaveBeenCalledTimes(1); 86 | 87 | // emit value 88 | act(() => onChange.mock.calls[0]![1](result1)); 89 | expect(result.current).toStrictEqual([result1, false, undefined]); 90 | 91 | // change ref 92 | rerender({ ref: refB1 }); 93 | expect(result.current).toStrictEqual([undefined, true, undefined]); 94 | expect(onChange).toHaveBeenCalledTimes(2); 95 | expect(onChangeUnsubscribe).toHaveBeenCalledTimes(1); 96 | 97 | // emit value 98 | act(() => onChange.mock.calls[1]![1](result2)); 99 | expect(result.current).toStrictEqual([result2, false, undefined]); 100 | }); 101 | 102 | it("from undefined ref to defined", () => { 103 | const { result, rerender } = renderHook( 104 | ({ ref }) => useListen(ref, onChange, isEqual, LoadingState), 105 | { 106 | initialProps: { ref: undefined }, 107 | }, 108 | ); 109 | 110 | expect(onChangeUnsubscribe).toHaveBeenCalledTimes(0); 111 | expect(onChange).toHaveBeenCalledTimes(0); 112 | 113 | rerender({ ref: refA1 }); 114 | expect(result.current).toStrictEqual([undefined, true, undefined]); 115 | 116 | expect(onChangeUnsubscribe).toHaveBeenCalledTimes(0); 117 | expect(onChange).toHaveBeenCalledTimes(1); 118 | }); 119 | 120 | it("from defined ref to undefined", () => { 121 | const { result, rerender } = renderHook(({ ref }) => useListen(ref, onChange, isEqual, LoadingState), { 122 | initialProps: { ref: refA1 }, 123 | }); 124 | 125 | expect(onChangeUnsubscribe).toHaveBeenCalledTimes(0); 126 | expect(onChange).toHaveBeenCalledTimes(1); 127 | 128 | rerender({ ref: undefined }); 129 | expect(result.current).toStrictEqual([undefined, false, undefined]); 130 | 131 | expect(onChangeUnsubscribe).toHaveBeenCalledTimes(1); 132 | expect(onChange).toHaveBeenCalledTimes(1); 133 | }); 134 | }); 135 | 136 | it("should return emitted values", () => { 137 | const { result } = renderHook(() => useListen(refA1, onChange, isEqual, LoadingState)); 138 | const setValue = onChange.mock.calls[0]![1]; 139 | 140 | expect(result.current).toStrictEqual([undefined, true, undefined]); 141 | 142 | act(() => setValue(result1)); 143 | expect(result.current).toStrictEqual([result1, false, undefined]); 144 | 145 | act(() => setValue(result2)); 146 | expect(result.current).toStrictEqual([result2, false, undefined]); 147 | }); 148 | 149 | it("should return emitted error", () => { 150 | const { result } = renderHook(() => useListen(refA1, onChange, isEqual, LoadingState)); 151 | const setValue = onChange.mock.calls[0]![1]; 152 | const setError = onChange.mock.calls[0]![2]; 153 | 154 | expect(result.current).toStrictEqual([undefined, true, undefined]); 155 | 156 | act(() => setError(error)); 157 | expect(result.current).toStrictEqual([undefined, false, error]); 158 | 159 | act(() => setValue(result2)); 160 | expect(result.current).toStrictEqual([result2, false, undefined]); 161 | }); 162 | 163 | it("resubscribes if `onChange` changes", async () => { 164 | const onChange1 = vi.fn().mockReturnValue(onChangeUnsubscribe); 165 | 166 | const { result, rerender } = renderHook(({ onChange }) => useListen(refA1, onChange, isEqual, LoadingState), { 167 | initialProps: { onChange: onChange1 }, 168 | }); 169 | const setValue1 = onChange1.mock.calls[0]![1]; 170 | act(() => setValue1(result1)); 171 | expect(result.current).toStrictEqual([result1, false, undefined]); 172 | 173 | const onChange2 = vi.fn().mockReturnValue(onChangeUnsubscribe); 174 | rerender({ onChange: onChange2 }); 175 | const setValue2 = onChange1.mock.calls[0]![1]; 176 | act(() => setValue2(result2)); 177 | expect(result.current).toStrictEqual([result2, false, undefined]); 178 | }); 179 | -------------------------------------------------------------------------------- /src/internal/useListen.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo } from "react"; 2 | import { ValueHookResult } from "../common/index.js"; 3 | import { useLoadingValue, LoadingState } from "./useLoadingValue.js"; 4 | import { useStableValue } from "./useStableValue.js"; 5 | import { usePrevious } from "./usePrevious.js"; 6 | 7 | /** 8 | * @internal 9 | */ 10 | export type UseListenOnChange = ( 11 | ref: Reference, 12 | onValue: (value: Value | undefined) => void, 13 | onError: (e: Error) => void, 14 | ) => () => void; 15 | 16 | /** 17 | * @internal 18 | */ 19 | export function useListen( 20 | reference: Reference | undefined, 21 | onChange: UseListenOnChange, 22 | isEqual: (a: Reference | undefined, b: Reference | undefined) => boolean, 23 | initialState: Value | typeof LoadingState, 24 | ): ValueHookResult { 25 | const { error, loading, setLoading, setError, setValue, value } = useLoadingValue( 26 | reference === undefined ? undefined : initialState, 27 | ); 28 | 29 | const stableRef = useStableValue(reference ?? undefined, isEqual); 30 | const previousRef = usePrevious(stableRef); 31 | 32 | // set state when ref changes 33 | useEffect(() => { 34 | if (stableRef === previousRef) { 35 | return; 36 | } 37 | 38 | if (stableRef === undefined) { 39 | setValue(); 40 | } else { 41 | setLoading(); 42 | } 43 | }, [previousRef, setLoading, setValue, stableRef]); 44 | 45 | // subscribe to changes 46 | useEffect(() => { 47 | if (stableRef === undefined) { 48 | return; 49 | } 50 | const unsubscribe = onChange(stableRef, setValue, setError); 51 | return () => unsubscribe(); 52 | }, [onChange, setError, setValue, stableRef]); 53 | 54 | return useMemo(() => [value, loading, error], [value, loading, error]); 55 | } 56 | -------------------------------------------------------------------------------- /src/internal/useLoadingValue.spec.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from "@testing-library/react"; 2 | import { newSymbol } from "../__testfixtures__/index.js"; 3 | import { LoadingState, useLoadingValue } from "./useLoadingValue.js"; 4 | import { it, expect, describe } from "vitest"; 5 | 6 | const value = newSymbol("Value"); 7 | const error = newSymbol("Error"); 8 | 9 | describe("initial state", () => { 10 | it("without default value", () => { 11 | const { result } = renderHook(() => useLoadingValue(LoadingState)); 12 | 13 | expect(result.current.value).toBeUndefined(); 14 | expect(result.current.loading).toBe(true); 15 | expect(result.current.error).toBeUndefined(); 16 | }); 17 | 18 | it("with default value", () => { 19 | const { result } = renderHook(() => useLoadingValue(value)); 20 | 21 | expect(result.current.value).toBe(value); 22 | expect(result.current.loading).toBe(false); 23 | expect(result.current.error).toBeUndefined(); 24 | }); 25 | 26 | it("with default value undefined", () => { 27 | const { result } = renderHook(() => useLoadingValue(undefined)); 28 | 29 | expect(result.current.value).toBe(undefined); 30 | expect(result.current.loading).toBe(false); 31 | expect(result.current.error).toBeUndefined(); 32 | }); 33 | }); 34 | 35 | describe("setValue", () => { 36 | it("with undefined value", () => { 37 | const { result } = renderHook(() => useLoadingValue(value)); 38 | act(() => result.current.setValue(undefined)); 39 | 40 | expect(result.current.value).toBeUndefined(); 41 | expect(result.current.loading).toBe(false); 42 | expect(result.current.error).toBeUndefined(); 43 | }); 44 | 45 | it("with a value", () => { 46 | const { result } = renderHook(() => useLoadingValue(LoadingState)); 47 | act(() => result.current.setValue(value)); 48 | 49 | expect(result.current.value).toBe(value); 50 | expect(result.current.loading).toBe(false); 51 | expect(result.current.error).toBeUndefined(); 52 | }); 53 | 54 | it("with error", () => { 55 | const { result } = renderHook(() => useLoadingValue(LoadingState)); 56 | act(() => result.current.setError(error)); 57 | 58 | act(() => result.current.setValue(value)); 59 | 60 | expect(result.current.value).toBe(value); 61 | expect(result.current.loading).toBe(false); 62 | expect(result.current.error).toBeUndefined(); 63 | }); 64 | }); 65 | 66 | describe("setError", () => { 67 | it("without value", () => { 68 | const { result } = renderHook(() => useLoadingValue(LoadingState)); 69 | act(() => result.current.setError(error)); 70 | 71 | expect(result.current.value).toBeUndefined(); 72 | expect(result.current.loading).toBe(false); 73 | expect(result.current.error).toBe(error); 74 | }); 75 | 76 | it("with value", () => { 77 | const { result } = renderHook(() => useLoadingValue(value)); 78 | act(() => result.current.setError(error)); 79 | 80 | expect(result.current.value).toBeUndefined(); 81 | expect(result.current.loading).toBe(false); 82 | expect(result.current.error).toBe(error); 83 | }); 84 | }); 85 | 86 | describe("setLoading", () => { 87 | it("with value", () => { 88 | const { result } = renderHook(() => useLoadingValue(value)); 89 | act(() => result.current.setLoading()); 90 | 91 | expect(result.current.value).toBeUndefined(); 92 | expect(result.current.loading).toBe(true); 93 | expect(result.current.error).toBeUndefined(); 94 | }); 95 | 96 | it("with error", () => { 97 | const { result } = renderHook(() => useLoadingValue(value)); 98 | act(() => result.current.setError(error)); 99 | 100 | act(() => result.current.setLoading()); 101 | 102 | expect(result.current.value).toBeUndefined(); 103 | expect(result.current.loading).toBe(true); 104 | expect(result.current.error).toBeUndefined(); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /src/internal/useLoadingValue.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useState } from "react"; 2 | 3 | export const LoadingState = Symbol(); 4 | 5 | /** 6 | * @internal 7 | */ 8 | interface State { 9 | value?: Value | undefined; 10 | loading: boolean; 11 | error?: Error | undefined; 12 | } 13 | 14 | /** 15 | * @internal 16 | */ 17 | export interface UseLoadingValueResult { 18 | value: Value | undefined; 19 | setValue: (value?: Value | undefined) => void; 20 | loading: boolean; 21 | setLoading: () => void; 22 | error?: Error | undefined; 23 | setError: (error: Error) => void; 24 | } 25 | 26 | /** 27 | * @internal 28 | */ 29 | export function useLoadingValue( 30 | initialState: Value | undefined | typeof LoadingState, 31 | ): UseLoadingValueResult { 32 | const [state, setState] = useState>({ 33 | error: undefined, 34 | loading: initialState === LoadingState ? true : false, 35 | value: initialState === LoadingState ? undefined : initialState, 36 | }); 37 | 38 | const setValue = useCallback((value?: Value | undefined) => { 39 | setState({ 40 | value, 41 | loading: false, 42 | error: undefined, 43 | }); 44 | }, []); 45 | 46 | const setLoading = useCallback(() => { 47 | setState({ 48 | value: undefined, 49 | loading: true, 50 | error: undefined, 51 | }); 52 | }, []); 53 | 54 | const setError = useCallback((error: Error) => { 55 | setState({ 56 | value: undefined, 57 | loading: false, 58 | error, 59 | }); 60 | }, []); 61 | 62 | return useMemo( 63 | () => ({ 64 | value: state.value, 65 | setValue, 66 | loading: state.loading, 67 | setLoading, 68 | error: state.error, 69 | setError, 70 | }), 71 | [state, setValue, setLoading, setError], 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/internal/useMultiGet.spec.ts: -------------------------------------------------------------------------------- 1 | import { configure, renderHook, waitFor } from "@testing-library/react"; 2 | import { newPromise, newSymbol } from "../__testfixtures__/index.js"; 3 | import { useMultiGet } from "./useMultiGet.js"; 4 | import { it, expect, beforeEach, vi, afterEach, describe } from "vitest"; 5 | 6 | const result1 = newSymbol("Result 1"); 7 | const result2 = newSymbol("Result 2"); 8 | 9 | const refA1 = newSymbol("Ref A1"); 10 | const refA2 = newSymbol("Ref A2"); 11 | 12 | const refB1 = newSymbol("Ref B1"); 13 | const refB2 = newSymbol("Ref B2"); 14 | 15 | const isEqual = (a: unknown, b: unknown) => 16 | [a, b].every((x) => [refA1, refA2].includes(x)) || [a, b].every((x) => [refB1, refB2].includes(x)); 17 | 18 | afterEach(() => { 19 | configure({ reactStrictMode: false }); 20 | }); 21 | 22 | describe.each([{ reactStrictMode: true }, { reactStrictMode: false }])( 23 | `strictMode=$reactStrictMode`, 24 | ({ reactStrictMode }) => { 25 | beforeEach(() => { 26 | configure({ reactStrictMode }); 27 | }); 28 | 29 | describe("initial state", () => { 30 | it("defined reference", () => { 31 | const getData = vi.fn().mockReturnValue(new Promise(() => {})); 32 | const { result } = renderHook(() => useMultiGet([refA1], getData, isEqual)); 33 | expect(result.current).toStrictEqual([[undefined, true, undefined]]); 34 | }); 35 | 36 | it("undefined reference", () => { 37 | const getData = vi.fn(); 38 | const { result } = renderHook(() => useMultiGet([], getData, isEqual)); 39 | expect(result.current).toStrictEqual([]); 40 | }); 41 | 42 | it("when changing ref count", () => {}); 43 | }); 44 | }, 45 | ); 46 | 47 | describe("changing refs", () => { 48 | it("should not fetch twice for equal ref", async () => { 49 | const { promise, resolve } = newPromise(); 50 | const getData = vi.fn().mockReturnValue(promise); 51 | 52 | // first ref 53 | const { result, rerender } = renderHook(({ refs }) => useMultiGet(refs, getData, isEqual), { 54 | initialProps: { refs: [refA1] }, 55 | }); 56 | 57 | expect(getData).toHaveBeenCalledTimes(1); 58 | 59 | // resolve value 60 | resolve(result1); 61 | await waitFor(() => expect(result.current).toStrictEqual([[result1, false, undefined]])); 62 | 63 | // change ref 64 | rerender({ refs: [refA2] }); 65 | expect(result.current).toStrictEqual([[result1, false, undefined]]); 66 | expect(getData).toHaveBeenCalledTimes(1); 67 | }); 68 | 69 | it("should refetch for unequal ref", async () => { 70 | const { promise: promise1, resolve: resolve1 } = newPromise(); 71 | const { promise: promise2, resolve: resolve2 } = newPromise(); 72 | const getData = vi.fn().mockReturnValueOnce(promise1).mockReturnValue(promise2); 73 | 74 | // first ref 75 | const { result, rerender } = renderHook(({ refs }) => useMultiGet(refs, getData, isEqual), { 76 | initialProps: { refs: [refA1] }, 77 | }); 78 | expect(getData).toHaveBeenCalledTimes(1); 79 | 80 | // emit value 81 | resolve1(result1); 82 | await waitFor(() => expect(result.current).toStrictEqual([[result1, false, undefined]])); 83 | 84 | // change ref 85 | rerender({ refs: [refB1] }); 86 | expect(result.current).toStrictEqual([[undefined, true, undefined]]); 87 | expect(getData).toHaveBeenCalledTimes(2); 88 | 89 | // emit value 90 | resolve2(result2); 91 | await waitFor(() => expect(result.current).toStrictEqual([[result2, false, undefined]])); 92 | }); 93 | 94 | it("should ignore the first result of `getData` if the ref changes before it resolves", async () => { 95 | const { promise: promise1, resolve: resolve1 } = newPromise(); 96 | const { promise: promise2, resolve: resolve2 } = newPromise(); 97 | const getData = vi.fn().mockReturnValueOnce(promise1).mockReturnValueOnce(promise2); 98 | 99 | // first ref 100 | const { result, rerender } = renderHook(({ ref }) => useMultiGet([ref], getData, isEqual), { 101 | initialProps: { ref: refA1 }, 102 | }); 103 | await waitFor(() => expect(result.current).toStrictEqual([[undefined, true, undefined]])); 104 | 105 | // change ref 106 | rerender({ ref: refB1 }); 107 | await waitFor(() => expect(result.current).toStrictEqual([[undefined, true, undefined]])); 108 | 109 | // first promise resolves 110 | resolve1(result1); 111 | 112 | // ensure that the first result is ignored 113 | await expect( 114 | waitFor( 115 | () => { 116 | expect(result.current).toStrictEqual([[result1, false, undefined]]); 117 | }, 118 | { timeout: 200 }, 119 | ), 120 | ).rejects.toThrow(); 121 | 122 | // second promise resolves 123 | resolve2(result2); 124 | await waitFor(() => expect(result.current).toStrictEqual([[result2, false, undefined]])); 125 | }); 126 | }); 127 | 128 | describe("changing size", () => { 129 | it("should adjust the returned states", async () => { 130 | const getData = vi.fn().mockReturnValue(new Promise(() => {})); 131 | 132 | // first ref 133 | const { result, rerender } = renderHook(({ refs }) => useMultiGet(refs, getData, isEqual), { 134 | initialProps: { refs: [refA1] }, 135 | }); 136 | await waitFor(() => expect(result.current).toStrictEqual([[undefined, true, undefined]])); 137 | 138 | // add ref 139 | rerender({ refs: [refA1, refB1] }); 140 | await waitFor(() => 141 | expect(result.current).toStrictEqual([ 142 | [undefined, true, undefined], 143 | [undefined, true, undefined], 144 | ]), 145 | ); 146 | 147 | // remove ref 148 | rerender({ refs: [refA1] }); 149 | await waitFor(() => expect(result.current).toStrictEqual([[undefined, true, undefined]])); 150 | }); 151 | 152 | it("increase", async () => { 153 | const { promise: promise1, resolve: resolve1 } = newPromise(); 154 | const { promise: promise2, resolve: resolve2 } = newPromise(); 155 | const getData = vi.fn().mockReturnValueOnce(promise1).mockReturnValueOnce(promise2); 156 | 157 | // first ref 158 | const { result, rerender } = renderHook(({ refs }) => useMultiGet(refs, getData, isEqual), { 159 | initialProps: { refs: [refA1] }, 160 | }); 161 | await waitFor(() => expect(result.current).toStrictEqual([[undefined, true, undefined]])); 162 | expect(getData).toHaveBeenCalledWith(refA1); 163 | expect(getData).toHaveBeenCalledTimes(1); 164 | 165 | // add ref 166 | rerender({ refs: [refA1, refB1] }); 167 | await waitFor(() => 168 | expect(result.current).toStrictEqual([ 169 | [undefined, true, undefined], 170 | [undefined, true, undefined], 171 | ]), 172 | ); 173 | expect(getData).toHaveBeenCalledWith(refB1); 174 | expect(getData).toHaveBeenCalledTimes(2); 175 | 176 | // first promise resolves 177 | resolve1(result1); 178 | await waitFor(() => 179 | expect(result.current).toStrictEqual([ 180 | [result1, false, undefined], 181 | [undefined, true, undefined], 182 | ]), 183 | ); 184 | 185 | // second promise resolves 186 | resolve2(result2); 187 | await waitFor(() => 188 | expect(result.current).toStrictEqual([ 189 | [result1, false, undefined], 190 | [result2, false, undefined], 191 | ]), 192 | ); 193 | }); 194 | 195 | it("decrease", async () => { 196 | const { promise: promise1, resolve: resolve1 } = newPromise(); 197 | const { promise: promise2, resolve: resolve2 } = newPromise(); 198 | const getData = vi.fn().mockReturnValueOnce(promise1).mockReturnValueOnce(promise2); 199 | 200 | // first ref 201 | const { result, rerender } = renderHook(({ refs }) => useMultiGet(refs, getData, isEqual), { 202 | initialProps: { refs: [refA1, refB1] }, 203 | }); 204 | await waitFor(() => 205 | expect(result.current).toStrictEqual([ 206 | [undefined, true, undefined], 207 | [undefined, true, undefined], 208 | ]), 209 | ); 210 | expect(getData).toHaveBeenCalledTimes(2); 211 | 212 | // second promise resolves 213 | resolve2(result2); 214 | await waitFor(() => 215 | expect(result.current).toStrictEqual([ 216 | [undefined, true, undefined], 217 | [result2, false, undefined], 218 | ]), 219 | ); 220 | 221 | // remove ref 222 | rerender({ refs: [refA1] }); 223 | await waitFor(() => expect(result.current).toStrictEqual([[undefined, true, undefined]])); 224 | expect(getData).toHaveBeenCalledTimes(2); 225 | 226 | // first promise resolves 227 | resolve1(result1); 228 | await waitFor(() => expect(result.current).toStrictEqual([[result1, false, undefined]])); 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /src/internal/useMultiGet.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useRef } from "react"; 2 | import { ValueHookResult } from "../common/index.js"; 3 | import { useIsMounted } from "./useIsMounted.js"; 4 | import { useMultiLoadingValue } from "./useMultiLoadingValue.js"; 5 | 6 | /** 7 | * @internal 8 | */ 9 | export function useMultiGet( 10 | references: ReadonlyArray, 11 | getData: (ref: Reference) => Promise, 12 | isEqual: (a: Reference | undefined, b: Reference | undefined) => boolean, 13 | ): ValueHookResult[] { 14 | const isMounted = useIsMounted(); 15 | 16 | const { states, setError, setLoading, setValue } = useMultiLoadingValue(references.length); 17 | const prevReferences = useRef([]); 18 | const ongoingFetchReferences = useRef([]); 19 | 20 | useEffect(() => { 21 | // shorten `prevReferences` size if number of references was reduced 22 | prevReferences.current = prevReferences.current.slice(0, references.length); 23 | 24 | // shorten `ongoingFetchReferences` size if number of references was reduced 25 | ongoingFetchReferences.current = ongoingFetchReferences.current.slice(0, references.length); 26 | 27 | // fetch to new references 28 | const changedReferences = references 29 | .map((ref, refIndex) => [ref, refIndex] as const) 30 | .filter(([ref, refIndex]) => !isEqual(ref, prevReferences.current[refIndex])); 31 | 32 | for (const [ref, refIndex] of changedReferences) { 33 | prevReferences.current[refIndex] = ref; 34 | setLoading(refIndex); 35 | ongoingFetchReferences.current[refIndex] = ref; 36 | 37 | (async () => { 38 | try { 39 | const data = await getData(ref); 40 | 41 | if (!isMounted.current) { 42 | return; 43 | } 44 | 45 | if (!isEqual(ongoingFetchReferences.current[refIndex], ref)) { 46 | return; 47 | } 48 | 49 | setValue(refIndex, data); 50 | } catch (e) { 51 | if (!isMounted.current) { 52 | return; 53 | } 54 | 55 | if (!isEqual(ongoingFetchReferences.current[refIndex], ref)) { 56 | return; 57 | } 58 | 59 | // We assume this is always a Error 60 | setError(refIndex, e as Error); 61 | } 62 | })(); 63 | } 64 | 65 | // TODO: double check dependencies 66 | // eslint-disable-next-line react-hooks/exhaustive-deps 67 | }, [references]); 68 | 69 | return useMemo( 70 | () => states.map((state) => [state.value, state.loading, state.error] as ValueHookResult), 71 | [states], 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/internal/useMultiListen.spec.ts: -------------------------------------------------------------------------------- 1 | import { act, configure, renderHook } from "@testing-library/react"; 2 | import { newSymbol } from "../__testfixtures__/index.js"; 3 | import { useMultiListen } from "./useMultiListen.js"; 4 | import { it, expect, beforeEach, describe, vi, afterEach } from "vitest"; 5 | 6 | const result1 = newSymbol("Result 1"); 7 | const result2 = newSymbol("Result 2"); 8 | const result3 = newSymbol("Result 3"); 9 | const result4 = newSymbol("Result 4"); 10 | const error1 = newSymbol("Error 1"); 11 | const error2 = newSymbol("Error 2"); 12 | 13 | const refA1 = newSymbol("Ref A1"); 14 | const refA2 = newSymbol("Ref A2"); 15 | 16 | const refB1 = newSymbol("Ref B1"); 17 | const refB2 = newSymbol("Ref B2"); 18 | 19 | const onChangeUnsubscribe = vi.fn(); 20 | const onChange = vi.fn(); 21 | 22 | const isEqual = (a: unknown, b: unknown) => 23 | [a, b].every((x) => [refA1, refA2].includes(x)) || [a, b].every((x) => [refB1, refB2].includes(x)); 24 | 25 | beforeEach(() => { 26 | vi.resetAllMocks(); 27 | onChange.mockReturnValue(onChangeUnsubscribe); 28 | }); 29 | 30 | afterEach(() => { 31 | configure({ reactStrictMode: false }); 32 | }); 33 | 34 | describe.each([{ reactStrictMode: true }, { reactStrictMode: false }])( 35 | `strictMode=$reactStrictMode`, 36 | ({ reactStrictMode }) => { 37 | beforeEach(() => { 38 | configure({ reactStrictMode }); 39 | }); 40 | 41 | it("initial state", () => { 42 | const { result } = renderHook(() => useMultiListen([refA1, refB1], onChange, isEqual)); 43 | expect(result.current).toStrictEqual([ 44 | [undefined, true, undefined], 45 | [undefined, true, undefined], 46 | ]); 47 | }); 48 | }, 49 | ); 50 | 51 | describe("when changing refs", () => { 52 | it("should not resubscribe for equal ref", async () => { 53 | // first ref 54 | const { result, rerender } = renderHook(({ refs }) => useMultiListen(refs, onChange, isEqual), { 55 | initialProps: { refs: [refA1] }, 56 | }); 57 | 58 | expect(onChangeUnsubscribe).toHaveBeenCalledTimes(0); 59 | expect(onChange).toHaveBeenCalledTimes(1); 60 | 61 | // emit value 62 | act(() => onChange.mock.calls[0]![1](result1)); 63 | expect(result.current).toStrictEqual([[result1, false, undefined]]); 64 | 65 | // change ref 66 | rerender({ refs: [refA2] }); 67 | expect(result.current).toStrictEqual([[result1, false, undefined]]); 68 | expect(onChangeUnsubscribe).toHaveBeenCalledTimes(0); 69 | expect(onChange).toHaveBeenCalledTimes(1); 70 | }); 71 | 72 | it("should resubscribe for different ref", () => { 73 | // first ref 74 | const { result, rerender } = renderHook(({ refs }) => useMultiListen(refs, onChange, isEqual), { 75 | initialProps: { refs: [refA1] }, 76 | }); 77 | expect(onChangeUnsubscribe).toHaveBeenCalledTimes(0); 78 | expect(onChange).toHaveBeenCalledTimes(1); 79 | 80 | // emit value 81 | act(() => onChange.mock.calls[0]![1](result1)); 82 | expect(result.current).toStrictEqual([[result1, false, undefined]]); 83 | 84 | // change ref 85 | rerender({ refs: [refB1] }); 86 | expect(result.current).toStrictEqual([[undefined, true, undefined]]); 87 | expect(onChange).toHaveBeenCalledTimes(2); 88 | expect(onChangeUnsubscribe).toHaveBeenCalledTimes(1); 89 | 90 | // emit value 91 | act(() => onChange.mock.calls[1]![1](result2)); 92 | expect(result.current).toStrictEqual([[result2, false, undefined]]); 93 | }); 94 | }); 95 | 96 | describe("changing size", () => { 97 | it("increase", () => { 98 | const { result, rerender } = renderHook(({ refs }) => useMultiListen(refs, onChange, isEqual), { 99 | initialProps: { refs: [refA1] }, 100 | }); 101 | 102 | const setValue1 = onChange.mock.calls[0]![1]; 103 | act(() => setValue1(result1)); 104 | expect(result.current).toStrictEqual([[result1, false, undefined]]); 105 | 106 | rerender({ refs: [refA1, refB1] }); 107 | expect(result.current).toStrictEqual([ 108 | [result1, false, undefined], 109 | [undefined, true, undefined], 110 | ]); 111 | 112 | const setValue2 = onChange.mock.calls[1]![1]; 113 | act(() => setValue2(result2)); 114 | expect(result.current).toStrictEqual([ 115 | [result1, false, undefined], 116 | [result2, false, undefined], 117 | ]); 118 | }); 119 | 120 | it("decrease", () => { 121 | const { result, rerender } = renderHook(({ refs }) => useMultiListen(refs, onChange, isEqual), { 122 | initialProps: { refs: [refA1, refB1] }, 123 | }); 124 | 125 | const setValue1 = onChange.mock.calls[0]![1]; 126 | const setValue2 = onChange.mock.calls[1]![1]; 127 | act(() => setValue1(result1)); 128 | act(() => setValue2(result2)); 129 | expect(result.current).toStrictEqual([ 130 | [result1, false, undefined], 131 | [result2, false, undefined], 132 | ]); 133 | 134 | rerender({ refs: [refA1] }); 135 | expect(onChangeUnsubscribe).toHaveBeenCalledTimes(1); 136 | expect(result.current).toStrictEqual([[result1, false, undefined]]); 137 | 138 | act(() => setValue1(result3)); 139 | expect(result.current).toStrictEqual([[result3, false, undefined]]); 140 | }); 141 | }); 142 | 143 | it("should return emitted values", () => { 144 | const { result } = renderHook(() => useMultiListen([refA1, refB1], onChange, isEqual)); 145 | const setValue1 = onChange.mock.calls[0]![1]; 146 | const setValue2 = onChange.mock.calls[1]![1]; 147 | 148 | expect(result.current).toStrictEqual([ 149 | [undefined, true, undefined], 150 | [undefined, true, undefined], 151 | ]); 152 | 153 | act(() => setValue1(result1)); 154 | expect(result.current).toStrictEqual([ 155 | [result1, false, undefined], 156 | [undefined, true, undefined], 157 | ]); 158 | 159 | act(() => setValue2(result3)); 160 | expect(result.current).toStrictEqual([ 161 | [result1, false, undefined], 162 | [result3, false, undefined], 163 | ]); 164 | 165 | act(() => setValue1(result2)); 166 | expect(result.current).toStrictEqual([ 167 | [result2, false, undefined], 168 | [result3, false, undefined], 169 | ]); 170 | 171 | act(() => setValue2(result4)); 172 | expect(result.current).toStrictEqual([ 173 | [result2, false, undefined], 174 | [result4, false, undefined], 175 | ]); 176 | }); 177 | 178 | it("should return emitted error", () => { 179 | const { result } = renderHook(() => useMultiListen([refA1, refB1], onChange, isEqual)); 180 | const setError1 = onChange.mock.calls[0]![2]; 181 | const setError2 = onChange.mock.calls[1]![2]; 182 | 183 | expect(result.current).toStrictEqual([ 184 | [undefined, true, undefined], 185 | [undefined, true, undefined], 186 | ]); 187 | 188 | act(() => setError1(error1)); 189 | expect(result.current).toStrictEqual([ 190 | [undefined, false, error1], 191 | [undefined, true, undefined], 192 | ]); 193 | 194 | act(() => setError2(error2)); 195 | expect(result.current).toStrictEqual([ 196 | [undefined, false, error1], 197 | [undefined, false, error2], 198 | ]); 199 | }); 200 | 201 | it("unmount", () => { 202 | const { unmount } = renderHook(() => useMultiListen([refA1, refB1], onChange, isEqual)); 203 | 204 | unmount(); 205 | 206 | expect(onChangeUnsubscribe).toHaveBeenCalledTimes(2); 207 | }); 208 | -------------------------------------------------------------------------------- /src/internal/useMultiListen.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useRef } from "react"; 2 | import { ValueHookResult } from "../common/index.js"; 3 | import { useMultiLoadingValue } from "./useMultiLoadingValue.js"; 4 | 5 | /** 6 | * @internal 7 | */ 8 | export type UseMultiListenChange = ( 9 | ref: Reference, 10 | onValue: (value: Value | undefined) => void, 11 | onError: (e: Error) => void, 12 | ) => () => void; 13 | 14 | /** 15 | * @internal 16 | */ 17 | export function useMultiListen( 18 | references: ReadonlyArray, 19 | onChange: UseMultiListenChange, 20 | isEqualRef: (a: Reference | undefined, b: Reference | undefined) => boolean, 21 | ): ValueHookResult[] { 22 | const { states, setError, setLoading, setValue } = useMultiLoadingValue(references.length); 23 | 24 | const prevReferences = useRef([]); 25 | const subscriptions = useRef<(() => void)[]>([]); 26 | 27 | useEffect(() => { 28 | // unsubscribe and shorten `subscriptions` if number of references was reduced 29 | subscriptions.current.slice(references.length).forEach((unsubscribe) => unsubscribe()); 30 | subscriptions.current = subscriptions.current.slice(0, references.length); 31 | 32 | // shorten `prevReferences` size if number of references was reduced 33 | prevReferences.current = prevReferences.current.slice(0, references.length); 34 | 35 | // subscribe to new references and unsubscribe to changed references 36 | const changedReferences = references 37 | .map((ref, refIndex) => [ref, refIndex] as const) 38 | .filter(([ref, refIndex]) => !isEqualRef(ref, prevReferences.current[refIndex])); 39 | 40 | for (const [ref, refIndex] of changedReferences) { 41 | subscriptions.current[refIndex]?.(); 42 | 43 | prevReferences.current[refIndex] = ref; 44 | setLoading(refIndex); 45 | subscriptions.current[refIndex] = onChange( 46 | ref, 47 | (snap) => setValue(refIndex, snap), 48 | (error) => setError(refIndex, error), 49 | ); 50 | } 51 | }, [references, isEqualRef, onChange, setError, setLoading, setValue]); 52 | 53 | // unsubscribe and cleanup on unmount 54 | useEffect( 55 | () => () => { 56 | subscriptions.current.forEach((unsubscribe) => unsubscribe()); 57 | subscriptions.current = []; 58 | 59 | prevReferences.current = []; 60 | }, 61 | [], 62 | ); 63 | 64 | return useMemo( 65 | () => states.map((state) => [state.value, state.loading, state.error] as ValueHookResult), 66 | [states], 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/internal/useMultiLoadingValue.spec.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from "@testing-library/react"; 2 | import { newSymbol } from "../__testfixtures__/index.js"; 3 | import { useMultiLoadingValue } from "./useMultiLoadingValue.js"; 4 | import { it, expect, describe } from "vitest"; 5 | 6 | const value1 = newSymbol("Value 1"); 7 | const value2 = newSymbol("Value 2"); 8 | const error1 = newSymbol("Error 1"); 9 | const error2 = newSymbol("Error 2"); 10 | 11 | it("initial state", () => { 12 | const { result } = renderHook(() => useMultiLoadingValue(1)); 13 | 14 | expect(result.current.states).toHaveLength(1); 15 | expect(result.current.states[0]!.value).toBeUndefined(); 16 | expect(result.current.states[0]!.loading).toBe(true); 17 | expect(result.current.states[0]!.error).toBeUndefined(); 18 | }); 19 | 20 | describe("size change", () => { 21 | it("increase", () => { 22 | const { result, rerender } = renderHook(({ size }) => useMultiLoadingValue(size), { 23 | initialProps: { size: 1 }, 24 | }); 25 | 26 | expect(result.current.states).toHaveLength(1); 27 | 28 | rerender({ size: 2 }); 29 | 30 | expect(result.current.states).toHaveLength(2); 31 | expect(result.current.states[0]!.value).toBeUndefined(); 32 | expect(result.current.states[0]!.loading).toBe(true); 33 | expect(result.current.states[0]!.error).toBeUndefined(); 34 | expect(result.current.states[1]!.value).toBeUndefined(); 35 | expect(result.current.states[1]!.loading).toBe(true); 36 | expect(result.current.states[1]!.error).toBeUndefined(); 37 | }); 38 | 39 | it("decrease", () => { 40 | const { result, rerender } = renderHook(({ size }) => useMultiLoadingValue(size), { 41 | initialProps: { size: 2 }, 42 | }); 43 | 44 | expect(result.current.states).toHaveLength(2); 45 | 46 | rerender({ size: 1 }); 47 | 48 | expect(result.current.states).toHaveLength(1); 49 | expect(result.current.states[0]!.value).toBeUndefined(); 50 | expect(result.current.states[0]!.loading).toBe(true); 51 | expect(result.current.states[0]!.error).toBeUndefined(); 52 | }); 53 | }); 54 | 55 | describe("setValue", () => { 56 | it("with undefined value", () => { 57 | const { result } = renderHook(() => useMultiLoadingValue(1)); 58 | act(() => result.current.setValue(0, undefined)); 59 | 60 | expect(result.current.states[0]!.value).toBeUndefined(); 61 | expect(result.current.states[0]!.loading).toBe(false); 62 | expect(result.current.states[0]!.error).toBeUndefined(); 63 | }); 64 | 65 | it("with a value", () => { 66 | const { result } = renderHook(() => useMultiLoadingValue(1)); 67 | act(() => result.current.setValue(0, value1)); 68 | 69 | expect(result.current.states[0]!.value).toBe(value1); 70 | expect(result.current.states[0]!.loading).toBe(false); 71 | expect(result.current.states[0]!.error).toBeUndefined(); 72 | }); 73 | 74 | it("multiple values", () => { 75 | const { result } = renderHook(() => useMultiLoadingValue(2)); 76 | act(() => result.current.setValue(0, value1)); 77 | act(() => result.current.setValue(1, value2)); 78 | 79 | expect(result.current.states[0]!.value).toBe(value1); 80 | expect(result.current.states[0]!.loading).toBe(false); 81 | expect(result.current.states[0]!.error).toBeUndefined(); 82 | 83 | expect(result.current.states[1]!.value).toBe(value2); 84 | expect(result.current.states[1]!.loading).toBe(false); 85 | expect(result.current.states[1]!.error).toBeUndefined(); 86 | }); 87 | 88 | it("with error", () => { 89 | const { result } = renderHook(() => useMultiLoadingValue(1)); 90 | act(() => result.current.setError(0, error1)); 91 | act(() => result.current.setValue(0, value1)); 92 | 93 | expect(result.current.states[0]!.value).toBe(value1); 94 | expect(result.current.states[0]!.loading).toBe(false); 95 | expect(result.current.states[0]!.error).toBeUndefined(); 96 | }); 97 | }); 98 | 99 | describe("setError", () => { 100 | it("without value", () => { 101 | const { result } = renderHook(() => useMultiLoadingValue(1)); 102 | act(() => result.current.setError(0, error1)); 103 | 104 | expect(result.current.states[0]!.value).toBeUndefined(); 105 | expect(result.current.states[0]!.loading).toBe(false); 106 | expect(result.current.states[0]!.error).toBe(error1); 107 | }); 108 | 109 | it("with value", () => { 110 | const { result } = renderHook(() => useMultiLoadingValue(1)); 111 | act(() => result.current.setValue(0, value1)); 112 | act(() => result.current.setError(0, error1)); 113 | 114 | expect(result.current.states[0]!.value).toBeUndefined(); 115 | expect(result.current.states[0]!.loading).toBe(false); 116 | expect(result.current.states[0]!.error).toBe(error1); 117 | }); 118 | 119 | it("multiple queries", () => { 120 | const { result } = renderHook(() => useMultiLoadingValue(2)); 121 | act(() => result.current.setError(0, error1)); 122 | act(() => result.current.setError(1, error2)); 123 | 124 | expect(result.current.states[0]!.value).toBeUndefined(); 125 | expect(result.current.states[0]!.loading).toBe(false); 126 | expect(result.current.states[0]!.error).toBe(error1); 127 | 128 | expect(result.current.states[1]!.value).toBeUndefined(); 129 | expect(result.current.states[1]!.loading).toBe(false); 130 | expect(result.current.states[1]!.error).toBe(error2); 131 | }); 132 | }); 133 | 134 | describe("setLoading", () => { 135 | it("with value", () => { 136 | const { result } = renderHook(() => useMultiLoadingValue(1)); 137 | act(() => result.current.setValue(0, value1)); 138 | act(() => result.current.setLoading(0)); 139 | 140 | expect(result.current.states[0]!.value).toBeUndefined(); 141 | expect(result.current.states[0]!.loading).toBe(true); 142 | expect(result.current.states[0]!.error).toBeUndefined(); 143 | }); 144 | 145 | it("with error", () => { 146 | const { result } = renderHook(() => useMultiLoadingValue(1)); 147 | act(() => result.current.setError(0, error1)); 148 | act(() => result.current.setLoading(0)); 149 | 150 | expect(result.current.states[0]!.value).toBeUndefined(); 151 | expect(result.current.states[0]!.loading).toBe(true); 152 | expect(result.current.states[0]!.error).toBeUndefined(); 153 | }); 154 | }); 155 | 156 | describe("combinations", () => { 157 | it("setError & setValue & setLoading", () => { 158 | const { result } = renderHook(() => useMultiLoadingValue(3)); 159 | act(() => result.current.setError(0, error1)); 160 | act(() => result.current.setValue(1, value2)); 161 | act(() => result.current.setLoading(2)); 162 | 163 | expect(result.current.states[0]!.value).toBeUndefined(); 164 | expect(result.current.states[0]!.loading).toBe(false); 165 | expect(result.current.states[0]!.error).toBe(error1); 166 | 167 | expect(result.current.states[1]!.value).toBe(value2); 168 | expect(result.current.states[1]!.loading).toBe(false); 169 | expect(result.current.states[1]!.error).toBeUndefined(); 170 | 171 | expect(result.current.states[2]!.value).toBeUndefined; 172 | expect(result.current.states[2]!.loading).toBe(true); 173 | expect(result.current.states[2]!.error).toBeUndefined(); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /src/internal/useMultiLoadingValue.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo, useState } from "react"; 2 | 3 | /** 4 | * @internal 5 | */ 6 | interface State { 7 | readonly value?: Value | undefined; 8 | readonly loading: boolean; 9 | readonly error?: Error | undefined; 10 | } 11 | 12 | /** 13 | * @internal 14 | */ 15 | export interface UseMultiLoadingValueResult { 16 | readonly states: ReadonlyArray>; 17 | readonly setValue: (index: number, value?: Value | undefined) => void; 18 | readonly setLoading: (index: number) => void; 19 | readonly setError: (index: number, error?: Error | undefined) => void; 20 | } 21 | 22 | const DEFAULT_STATE = { 23 | error: undefined, 24 | loading: true, 25 | value: undefined, 26 | }; 27 | 28 | /** 29 | * @internal 30 | */ 31 | export function useMultiLoadingValue(size: number): UseMultiLoadingValueResult { 32 | const [states, setState] = useState[]>(() => Array.from({ length: size }).map(() => DEFAULT_STATE)); 33 | 34 | const setValue = useCallback((index: number, value?: Value | undefined) => { 35 | setState((curStates) => 36 | curStates.map((state, stateIndex) => 37 | stateIndex === index 38 | ? { 39 | value: value, 40 | loading: false, 41 | error: undefined, 42 | } 43 | : state, 44 | ), 45 | ); 46 | }, []); 47 | 48 | const setLoading = useCallback((index: number) => { 49 | setState((curStates) => 50 | curStates.map((state, stateIndex) => 51 | stateIndex === index 52 | ? { 53 | value: undefined, 54 | loading: true, 55 | error: undefined, 56 | } 57 | : state, 58 | ), 59 | ); 60 | }, []); 61 | 62 | const setError = useCallback((index: number, error?: Error | undefined) => { 63 | setState((curStates) => 64 | curStates.map((state, stateIndex) => 65 | stateIndex === index 66 | ? { 67 | value: undefined, 68 | loading: false, 69 | error, 70 | } 71 | : state, 72 | ), 73 | ); 74 | }, []); 75 | 76 | useEffect(() => { 77 | if (states.length === size) { 78 | return; 79 | } 80 | 81 | setState((curStates) => { 82 | if (curStates.length > size) { 83 | return curStates.slice(0, size); 84 | } else if (curStates.length < size) { 85 | return [...curStates, ...Array.from({ length: size - curStates.length }).map(() => DEFAULT_STATE)]; 86 | /* c8 ignore next 3 */ 87 | } else { 88 | return curStates; 89 | } 90 | }); 91 | }, [size, states.length]); 92 | 93 | return useMemo(() => ({ states, setValue, setLoading, setError }), [states, setValue, setLoading, setError]); 94 | } 95 | -------------------------------------------------------------------------------- /src/internal/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | export function usePrevious(value: T) { 4 | const ref = useRef(value); 5 | useEffect(() => { 6 | ref.current = value; 7 | }); 8 | return ref.current; 9 | } 10 | -------------------------------------------------------------------------------- /src/internal/useStableValue.spec.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from "@testing-library/react"; 2 | import { newSymbol } from "../__testfixtures__/index.js"; 3 | import { useStableValue } from "./useStableValue.js"; 4 | import { it, expect } from "vitest"; 5 | 6 | const value1 = newSymbol("Value 1"); 7 | const value2 = newSymbol("Value 2"); 8 | 9 | it("should return same value if values are equal", () => { 10 | const alwaysTrue = () => true; 11 | 12 | const { result, rerender } = renderHook(({ value }) => useStableValue(value, alwaysTrue), { 13 | initialProps: { value: value1 }, 14 | }); 15 | 16 | expect(result.current).toBe(value1); 17 | rerender({ value: value2 }); 18 | expect(result.current).toBe(value1); 19 | }); 20 | 21 | it("should return same value if values are not equal", () => { 22 | const alwaysFalse = () => false; 23 | 24 | const { result, rerender } = renderHook(({ value }) => useStableValue(value, alwaysFalse), { 25 | initialProps: { value: value1 }, 26 | }); 27 | 28 | expect(result.current).toBe(value1); 29 | rerender({ value: value2 }); 30 | expect(result.current).toBe(value2); 31 | }); 32 | -------------------------------------------------------------------------------- /src/internal/useStableValue.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | /** 4 | * @internal 5 | */ 6 | export function useStableValue(value: Value, isEqual: (a: Value, b: Value) => boolean): Value { 7 | const [state, setState] = useState(value); 8 | 9 | useEffect(() => { 10 | if (!isEqual(state, value)) { 11 | setState(value); 12 | } 13 | 14 | // TODO: double check dependencies 15 | // eslint-disable-next-line react-hooks/exhaustive-deps 16 | }, [value]); 17 | 18 | return state; 19 | } 20 | -------------------------------------------------------------------------------- /src/messaging/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useMessagingToken.js"; 2 | -------------------------------------------------------------------------------- /src/messaging/useMessagingToken.ts: -------------------------------------------------------------------------------- 1 | import { Messaging, getToken, GetTokenOptions } from "firebase/messaging"; 2 | import { ValueHookResult } from "../common/index.js"; 3 | import { useGet } from "../internal/useGet.js"; 4 | 5 | export type UseMessagingTokenResult = ValueHookResult; 6 | 7 | /** 8 | * Options to configure how the token will be fetched 9 | */ 10 | export interface UseMessagingTokenOptions { 11 | getTokenOptions?: GetTokenOptions | undefined; 12 | } 13 | 14 | /** 15 | * Returns the messaging token. The token never updates. 16 | * @param messaging Firebase Messaging instance 17 | * @param options Options to configure how the token will be fetched 18 | * @returns Token, loading state, and error 19 | * - value: Messaging token; `undefined` if token is currently being fetched, or an error occurred 20 | * - loading: `true` while fetching the token; `false` if the token was fetched successfully or an error occurred 21 | * - error: `undefined` if no error occurred 22 | */ 23 | export function useMessagingToken( 24 | messaging: Messaging, 25 | options?: UseMessagingTokenOptions | undefined, 26 | ): UseMessagingTokenResult { 27 | return useGet( 28 | messaging, 29 | (m) => getToken(m, options?.getTokenOptions), 30 | () => true, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/storage/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useBlob.js"; 2 | export * from "./useBytes.js"; 3 | export * from "./useDownloadURL.js"; 4 | export * from "./useMetadata.js"; 5 | export * from "./useStream.js"; 6 | -------------------------------------------------------------------------------- /src/storage/internal.spec.ts: -------------------------------------------------------------------------------- 1 | import { StorageReference } from "@firebase/storage"; 2 | import { describe, expect, it } from "vitest"; 3 | import { isStorageRefEqual } from "./internal.js"; 4 | 5 | describe("isStorageRefEqual", () => { 6 | it("returns true for undefined reference", () => { 7 | expect(isStorageRefEqual(undefined, undefined)).toBe(true); 8 | }); 9 | 10 | it("returns false for undefined and defined reference", () => { 11 | const ref = { fullPath: "path" } as StorageReference; 12 | 13 | expect(isStorageRefEqual(ref, undefined)).toBe(false); 14 | expect(isStorageRefEqual(undefined, ref)).toBe(false); 15 | }); 16 | 17 | it("returns false for different paths", () => { 18 | const ref1 = { fullPath: "path1" } as StorageReference; 19 | const ref2 = { fullPath: "path2" } as StorageReference; 20 | 21 | expect(isStorageRefEqual(ref1, ref2)).toBe(false); 22 | }); 23 | 24 | it("returns true for same paths", () => { 25 | const ref1 = { fullPath: "path" } as StorageReference; 26 | const ref2 = { fullPath: "path" } as StorageReference; 27 | 28 | expect(isStorageRefEqual(ref1, ref2)).toBe(true); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/storage/internal.ts: -------------------------------------------------------------------------------- 1 | import { StorageReference } from "firebase/storage"; 2 | 3 | /** 4 | * @internal 5 | */ 6 | export function isStorageRefEqual(a: StorageReference | undefined, b: StorageReference | undefined): boolean { 7 | return a?.fullPath === b?.fullPath; 8 | } 9 | -------------------------------------------------------------------------------- /src/storage/useBlob.ts: -------------------------------------------------------------------------------- 1 | import { getBlob, StorageError, StorageReference } from "firebase/storage"; 2 | import { useCallback } from "react"; 3 | import { ValueHookResult } from "../common/index.js"; 4 | import { useGet } from "../internal/useGet.js"; 5 | import { isStorageRefEqual } from "./internal.js"; 6 | 7 | export type UseBlobResult = ValueHookResult; 8 | 9 | /** 10 | * Returns the data of a Google Cloud Storage object as a Blob 11 | * 12 | * This hook is not available in Node 13 | * @param reference Reference to a Google Cloud Storage object 14 | * @param maxDownloadSizeBytes If set, the maximum allowed size in bytes to retrieve 15 | * @returns Data, loading state, and error 16 | * - value: Object data as a Blob; `undefined` if data of the object is currently being downloaded, or an error occurred 17 | * - loading: `true` while downloading the data of the object; `false` if the data was downloaded successfully or an error occurred 18 | * - error: `undefined` if no error occurred 19 | */ 20 | export function useBlob( 21 | reference: StorageReference | undefined | null, 22 | maxDownloadSizeBytes?: number | undefined, 23 | ): UseBlobResult { 24 | const fetchBlob = useCallback( 25 | async (ref: StorageReference) => getBlob(ref, maxDownloadSizeBytes), 26 | [maxDownloadSizeBytes], 27 | ); 28 | 29 | return useGet(reference ?? undefined, fetchBlob, isStorageRefEqual); 30 | } 31 | -------------------------------------------------------------------------------- /src/storage/useBytes.ts: -------------------------------------------------------------------------------- 1 | import { getBytes, StorageError, StorageReference } from "firebase/storage"; 2 | import { useCallback } from "react"; 3 | import { ValueHookResult } from "../common/index.js"; 4 | import { useGet } from "../internal/useGet.js"; 5 | import { isStorageRefEqual } from "./internal.js"; 6 | 7 | export type UseBytesResult = ValueHookResult; 8 | 9 | /** 10 | * Returns the data of a Google Cloud Storage object 11 | * @param reference Reference to a Google Cloud Storage object 12 | * @param maxDownloadSizeBytes If set, the maximum allowed size in bytes to retrieve 13 | * @returns Data, loading state, and error 14 | * - value: Object data; `undefined` if data of the object is currently being downloaded, or an error occurred 15 | * - loading: `true` while downloading the data of the object; `false` if the data was downloaded successfully or an error occurred 16 | * - error: `undefined` if no error occurred 17 | */ 18 | export function useBytes( 19 | reference: StorageReference | undefined | null, 20 | maxDownloadSizeBytes?: number | undefined, 21 | ): UseBytesResult { 22 | const fetchBytes = useCallback( 23 | async (ref: StorageReference) => getBytes(ref, maxDownloadSizeBytes), 24 | [maxDownloadSizeBytes], 25 | ); 26 | 27 | return useGet(reference ?? undefined, fetchBytes, isStorageRefEqual); 28 | } 29 | -------------------------------------------------------------------------------- /src/storage/useDownloadURL.ts: -------------------------------------------------------------------------------- 1 | import { getDownloadURL, StorageError, StorageReference } from "firebase/storage"; 2 | import { ValueHookResult } from "../common/index.js"; 3 | import { useGet } from "../internal/useGet.js"; 4 | import { isStorageRefEqual } from "./internal.js"; 5 | 6 | export type UseDownloadURLResult = ValueHookResult; 7 | 8 | /** 9 | * Returns the download URL of a Google Cloud Storage object 10 | * @param reference Reference to a Google Cloud Storage object 11 | * @returns Download URL, loading state, and error 12 | * - value: Download URL; `undefined` if download URL is currently being fetched, or an error occurred 13 | * - loading: `true` while fetching the download URL; `false` if the download URL was fetched successfully or an error occurred 14 | * - error: `undefined` if no error occurred 15 | */ 16 | export function useDownloadURL(reference: StorageReference | undefined | null): UseDownloadURLResult { 17 | return useGet(reference ?? undefined, getDownloadURL, isStorageRefEqual); 18 | } 19 | -------------------------------------------------------------------------------- /src/storage/useMetadata.ts: -------------------------------------------------------------------------------- 1 | import { FullMetadata, getMetadata, StorageError, StorageReference } from "firebase/storage"; 2 | import type { ValueHookResult } from "../common/index.js"; 3 | import { useGet } from "../internal/useGet.js"; 4 | import { isStorageRefEqual } from "./internal.js"; 5 | 6 | export type UseMetadataResult = ValueHookResult; 7 | 8 | /** 9 | * Returns the metadata of a Google Cloud Storage object 10 | * @param reference Reference to a Google Cloud Storage object 11 | * @returns Metadata, loading state, and error 12 | * - value: Metadata; `undefined` if metadata is currently being fetched, or an error occurred 13 | * - loading: `true` while fetching the metadata; `false` if the metadata was fetched successfully or an error occurred 14 | * - error: `undefined` if no error occurred 15 | */ 16 | export function useMetadata(reference: StorageReference | undefined | null): UseMetadataResult { 17 | return useGet(reference ?? undefined, getMetadata, isStorageRefEqual); 18 | } 19 | -------------------------------------------------------------------------------- /src/storage/useStream.ts: -------------------------------------------------------------------------------- 1 | import { getStream, StorageError, StorageReference } from "firebase/storage"; 2 | import { useCallback } from "react"; 3 | import type { ValueHookResult } from "../common/index.js"; 4 | import { useGet } from "../internal/useGet.js"; 5 | import { isStorageRefEqual } from "./internal.js"; 6 | 7 | export type UseStreamResult = ValueHookResult; 8 | 9 | /** 10 | * Returns the data of a Google Cloud Storage object as a stream 11 | * 12 | * This hook is only available in Node 13 | * @param reference Reference to a Google Cloud Storage object 14 | * @param maxDownloadSizeBytes If set, the maximum allowed size in bytes to retrieve 15 | * @returns Data, loading state, and error 16 | * - value: Object data as stream; `undefined` if data of the object is currently being downloaded, or an error occurred 17 | * - loading: `true` while downloading the data of the object; `false` if the data was downloaded successfully or an error occurred 18 | * - error: `undefined` if no error occurred 19 | */ 20 | export function useStream( 21 | reference: StorageReference | undefined | null, 22 | maxDownloadSizeBytes?: number | undefined, 23 | ): UseStreamResult { 24 | const fetchBlob = useCallback( 25 | async (ref: StorageReference) => getStream(ref, maxDownloadSizeBytes), 26 | [maxDownloadSizeBytes], 27 | ); 28 | 29 | return useGet(reference ?? undefined, fetchBlob, isStorageRefEqual); 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@tsconfig/recommended/tsconfig.json", "@tsconfig/strictest/tsconfig.json"], 3 | "compilerOptions": { 4 | "module": "Node16", 5 | "target": "ES2017", 6 | "declaration": true, 7 | "outDir": "lib" 8 | }, 9 | "files": ["src/index.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/index.ts"], 3 | "out": "typedocs", 4 | "readme": "none" 5 | } 6 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: "happy-dom", 6 | coverage: { 7 | provider: "v8", 8 | }, 9 | }, 10 | }); 11 | --------------------------------------------------------------------------------