{"_id":"use-firestore","_rev":"38-24b1715c88fce9a89e68a3e035ffdea1","name":"use-firestore","dist-tags":{"latest":"0.17.0","next":"0.18.0-beta.3"},"versions":{"0.2.0":{"name":"use-firestore","version":"0.2.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces ","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","test":"vitest run --config vite.test.config.js","watch:test":"vitest watch --config vite.test.config.js"},"types":"./dist/lib.umd.d.ts","gitHead":"f98c7f96e6c8005473eede2214f484161f89853c","_id":"use-firestore@0.2.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-i5f/hyqouG+RKcLqNLWX284cl1nr343D8VJNOzvfQYGF0vgY34yzNdVNSy57bEv+xcgUYn6YCnC2/mT6Ozo5LA==","shasum":"286e82a77d5d1d87f6538c0011347f73ef16bf46","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.2.0.tgz","fileCount":9,"unpackedSize":3509383,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIBhJcFRQ2n3yeVpwNLJu4R+7XXl0DD2yxFOdlSCBc7XlAiEAtPgyHdohGOuNVGSqMC0VF2q47uNgT+Ayz5O85RsvLNQ="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.2.0_1685266023872_0.21127313850862683"},"_hasShrinkwrap":false},"0.3.0":{"name":"use-firestore","version":"0.3.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces ","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","test":"vitest run --config vite.test.config.js","watch:test":"vitest watch --config vite.test.config.js"},"types":"./dist/lib.umd.d.ts","gitHead":"1adfc2949cbf4a3a572dc770339d2c7d1eed616a","description":"**use-firestore** provides a set of React hooks which let you load Firestore data at the component level.","_id":"use-firestore@0.3.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-FejUmTC545rTzkSGVRuBdYCQXds0G2kgbLUAoZXMaVBrR4m0rJaMXsOASlvcGCvgUTSuh6+N/9+wqL1dgVKxgg==","shasum":"6a9e1202f550317dcb0b05a56ea8d9aeddf8918a","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.3.0.tgz","fileCount":11,"unpackedSize":3512045,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIGrC81Fdiej6OhKpKfbTcktu8PIAJeZSIEGcK1CPCF30AiEAhBtqpD7FVBd73r5WIKHEFft82RVks2HDtatoxrtvf2A="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.3.0_1685266999793_0.7061340826261255"},"_hasShrinkwrap":false},"0.4.0":{"name":"use-firestore","version":"0.4.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces ","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","test":"vitest run --config vite.test.config.js","watch:test":"vitest watch --config vite.test.config.js"},"types":"./dist/lib.umd.d.ts","gitHead":"6486adae78045e2f985e0609a457a7a498bf4e40","description":"**use-firestore** provides a set of React hooks which let you load Firestore data at the component level.","_id":"use-firestore@0.4.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-PP8L5RQ/U1wLr3MyJ8FH7I4VxDhckQGjiDMRaaJV3KRTXAwQUn6L5jX7ePBvatX8nw6yxouiaCSLipj6uRgGYA==","shasum":"d8e95879316c68e65f31a11eb528896d29d7b610","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.4.0.tgz","fileCount":11,"unpackedSize":3527927,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIB7OQd6zIlImAQbHiO8unFOKKGkvwI4xbaoEXzY5CfAGAiEA4O+BQhBny/m4GyiQ68QN1dC1lvPm8kPfQ1gEkgD0XnI="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.4.0_1685319388438_0.14594715091846777"},"_hasShrinkwrap":false},"0.5.0":{"name":"use-firestore","version":"0.5.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces ","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","test":"vitest run --config vite.test.config.js","watch:test":"vitest watch --config vite.test.config.js"},"types":"./dist/lib.umd.d.ts","gitHead":"fc568a6eca8729c210f032cd79597edea7191c59","description":"**use-firestore** provides a set of React hooks which let you load Firestore data at the component level.","_id":"use-firestore@0.5.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-r95hXlp7BafkKIbwlNa6veFVooLw8Jo+nya8SIBgdZQg3oMDY9HLqEnLkoVdmVMQyGkCWbRnBfNMtira6JQzqA==","shasum":"28bc6828fe9652e7c7cbbafca99808cc5afe5dca","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.5.0.tgz","fileCount":11,"unpackedSize":3527912,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIDpVXGB71Wcxod+TWlN6xSaiQpHCPwPrxluW8LM1yOuBAiBejmRgfBFQEmslimT37p8KNNDD08+wOaeMoy1lmKY+VA=="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.5.0_1685319928158_0.9013370834941445"},"_hasShrinkwrap":false},"0.6.0":{"name":"use-firestore","version":"0.6.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces ","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","test":"vitest run --config vite.test.config.js","watch:test":"vitest watch --config vite.test.config.js"},"types":"./dist/lib.umd.d.ts","gitHead":"a779aed65b309510194dc5d3d99af2eb373ea997","description":"**use-firestore** provides a set of React hooks which let you load Firestore data at the component level.","_id":"use-firestore@0.6.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-mN++DM9le/iMirXQuTjUh69avS81FXuEX300p5XVro4QIBoLFxJhc7Jpq3APu95qWShspU+6xFMXWWwPTLCm3w==","shasum":"aedd37999da2e27b6c2c0303e7a02155db48fafc","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.6.0.tgz","fileCount":11,"unpackedSize":3527936,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDua4dEnHGe20pKkfyqb5HvCtBBjpnn2La97HCSTv9skAIgM7vkwWG1CsQNKEabnYIfmnn3RA6IYmbtaTnpFsH3uXo="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.6.0_1685320071628_0.8695861073214619"},"_hasShrinkwrap":false},"0.7.0-beta.0":{"name":"use-firestore","version":"0.7.0-beta.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","test":"vitest run --config vite.test.config.js","watch:test":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"**use-firestore** provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Example code](#example-code)\n- [Why](#why)\n- [Todo](#todo)\n\n### What it does\n\nIt does this by caching results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `query` object doesn't need to be the same object for this to work, as long as\nits the same path, filters, and conditions it will produce a cache hit.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useDocs<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\n### Example code\n\n```tsx\nimport { useDocs, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useDocs(\n    query(collection(getFirestore(app), \"users\"), where(\"teamId\", \"==\", teamId))\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useDocs, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useDocs(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useDocs(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useDocs, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useDocs(query(collection(getFirestore(app), \"stories\")))\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useDocs(query(collection(getFirestore(app), \"tags\")))\n  const tagsById = useGlobalMemo(\"tagsById\", () => tags && keyBy(tags), [tags])\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n### Todo\n\n- [ ] Unsubscribe from query when no more listeners are left\n- [ ] Add tests\n","readmeFilename":"README.md","gitHead":"9bc48bedb81176925e36e1f4582b4782de900bb4","description":"**use-firestore** provides a set of React hooks which let you load Firestore data at the component level.","_id":"use-firestore@0.7.0-beta.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-Fzm9cw6y0IQT8+12m+O0+6pQH+fiAoY/+R8mlKMk9VhhkexHSHtsuNw1Z3bx3lYY5vxZF4Xxil5a211JuIraQw==","shasum":"b938fb921176ea89d74600a028314c4748b1a4c6","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.7.0-beta.0.tgz","fileCount":11,"unpackedSize":45152,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIE9nkgP+oo3G6Rb8QoNRFHTls3cx0VIXBj6r1VgYE02KAiA18xPMtylgk1ELOml2VfqnLXwrsn8ETJzH8/U3MD3XsA=="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.7.0-beta.0_1686332020703_0.24151269366321015"},"_hasShrinkwrap":false},"0.7.0":{"name":"use-firestore","version":"0.7.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","test":"vitest run --config vite.test.config.js","watch:test":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","gitHead":"2e63efd0f255b97ac4199db033b50461fd7c56fc","description":"**use-firestore** provides a set of React hooks which let you load Firestore data at the component level.","_id":"use-firestore@0.7.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-BavMF+8hbaLaVFlq2sO1QbIfdxv3hT3SB491oALmVxxaVPPIrt0iw0YWPAByh3usuaW1p+M7yLeLNXvhlbl9qw==","shasum":"e153aba280144590132f2f68b6e9bd73552d0879","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.7.0.tgz","fileCount":11,"unpackedSize":45145,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIBMJ7Qj1N/DlzdjPg2bH0fgp3vuiQmbXSb6QzY90U06MAiEAwpSRyCqiJnSSPcydp/i9YwawRcVUr/ybJTAb70QmZs0="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.7.0_1686333621380_0.5365684220528741"},"_hasShrinkwrap":false},"0.8.0":{"name":"use-firestore","version":"0.8.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","test":"vitest run --config vite.test.config.js","watch:test":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","gitHead":"59b55670e499161ea7412681e963f47535f457f8","description":"**use-firestore** provides a set of React hooks which let you load Firestore data at the component level.","_id":"use-firestore@0.8.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-cTbZ26iUyqE3yb1GFEA7M0cv48v1IZGOYzA1ApPuZiFWrT/ItT8VPct7ETtCEXt5hfehTKUU+aZe6cmsYGMsIQ==","shasum":"184130a916530bcffc799b23f1cc9fb8ad54d44f","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.8.0.tgz","fileCount":11,"unpackedSize":45258,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCCK9/3JAc/iLu32qjppkKSaPJURVSPFE4/JWxTquF3BQIhAOB+fzH/Ra5b2wY9tBCsIbdUC8aEycIimEkj1OQX6ko0"}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.8.0_1686523955038_0.9026126636723593"},"_hasShrinkwrap":false},"0.9.0-beta.0":{"name":"use-firestore","version":"0.9.0-beta.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","watch:test":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Example code](#example-code)\n- [Why](#why)\n- [Todo](#todo)\n\n### What it does\n\nIt does this by caching results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `query` object doesn't need to be the same object for this to work, as long as\nits the same path, filters, and conditions it will produce a cache hit.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useDocs<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\n### `useDocs` hook\n\n```tsx\nimport { useDocs, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useDocs(\n    query(collection(getFirestore(app), \"users\"), where(\"teamId\", \"==\", teamId))\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useDocs, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useDocs(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useDocs(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useDocs, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useDocs(query(collection(getFirestore(app), \"stories\")))\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useDocs(query(collection(getFirestore(app), \"tags\")))\n  const tagsById = useGlobalMemo(\"tagsById\", () => tags && keyBy(tags), [tags])\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n### Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [ ] useAssociationLookup()\n- [ ] useCascadingDelete()\n","readmeFilename":"README.md","gitHead":"35368b1a3b238eeefefd9859e75e64c5543666e7","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.9.0-beta.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-5gXK3T2ZJaL71yuWpNT+1G+pTV5WSLNVMA+J5RmNJr7qAzwL9irXNoDF8zJdbOfegd565Mkold/yf5QGBo7Iig==","shasum":"62f62ab16fce9eca00b7e384bbb4981d4c2c3c4b","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.9.0-beta.0.tgz","fileCount":22,"unpackedSize":226599,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIGhb0cymTRq625COUoAA97SxSX1C+MfyTWSOPFTOdwqtAiEA/6HAsMekt27of06yKVXmcD85+xBGOt7Oia3+vr2fT4g="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.9.0-beta.0_1688486582795_0.9923893691804431"},"_hasShrinkwrap":false},"0.10.0-beta.0":{"name":"use-firestore","version":"0.10.0-beta.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Example code](#example-code)\n- [Why](#why)\n- [Todo](#todo)\n\n### What it does\n\nIt does this by caching results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `query` object doesn't need to be the same object for this to work, as long as\nits the same path, filters, and conditions it will produce a cache hit.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({ slug, tagIds }: { slug: string; tagIds: string[] }) {\n  const tags = useDocs<Tag>(collection(getFirestore(testApp), \"tags\"), tagIds)\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out @chrisbianca's [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can doa single query and get all the data you need.\n\nIf you want to take this \"denormalized\" approach check out the [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that the synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                  | use-firestore | react-firebase-hooks                |\n| ------------------------------------------------ | ------------- | ----------------------------------- |\n| React-based                                      | ✅            | ✅                                  |\n| Realtime updates                                 | ✅            | ✅                                  |\n| Re-use queries application-wide                  | ✅            | ❌                                  |\n| Throws errors                                    | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                               | ✅            | ❌                                  |\n| Batch document reads to avoid N+1 problem        | ✅            | ❌                                  |\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(collection(getFirestore(app), \"users\"), where(\"teamId\", \"==\", teamId))\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(collection(getFirestore(app), \"tags\"), tagIds)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(query(collection(getFirestore(app), \"stories\")))\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(query(collection(getFirestore(app), \"tags\")))\n  const tagsById = useGlobalMemo(\"tagsById\", () => tags && keyBy(tags), [tags])\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n### Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [ ] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n","readmeFilename":"README.md","gitHead":"0cb68deb9664e84f1a1040234c3bb7ea3d0cb438","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.10.0-beta.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-lJp7sIfnZnMIiFLe9/z709v/QWpQNkEuNcGF4ArOfJgzonZLt/QSZbdVnAlmVPtuVZoTn4dAXfUe4xnGfcAOZw==","shasum":"aa712b7b8d7a8e46bd2d4c4811152396b498f0d6","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.10.0-beta.0.tgz","fileCount":32,"unpackedSize":271249,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIA89AbM5mfZS5sZiDxfhbL21/5+oTVN5F4AvBT2NOA+aAiEA2M84IN4Y17YY7It/w2d/phiec8qhIgY0UaElgxRTC8o="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.10.0-beta.0_1689880763070_0.3787834319732921"},"_hasShrinkwrap":false},"0.10.0-beta.1":{"name":"use-firestore","version":"0.10.0-beta.1","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","gitHead":"092643ea295f8a873f9b1734d6280ff09f114d05","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.10.0-beta.1","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-yZnduWTLomPM8pi6zoPt90F+6RowoX05V52RglVL/xBmpUDJlP+PsKloz0xE7I8CP2b1Ah3Hz9aBVjwsjhb7uw==","shasum":"66cf844c05ef8990f12d407cc22a649770793847","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.10.0-beta.1.tgz","fileCount":26,"unpackedSize":257437,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCcjQJgaLODI5WdGBfziokTc9MkCbMGsQnUP5xTTjKYHwIgC10vCnabMa7gowqEDpFoBnGTF2OkO8isvdRFR8s7dRs="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.10.0-beta.1_1689881104359_0.15699265950609353"},"_hasShrinkwrap":false},"0.10.0-beta.2":{"name":"use-firestore","version":"0.10.0-beta.2","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Example code](#example-code)\n- [Why](#why)\n- [Todo](#todo)\n\n### What it does\n\nIt does this by caching results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `query` object doesn't need to be the same object for this to work, as long as\nits the same path, filters, and conditions it will produce a cache hit.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({ slug, tagIds }: { slug: string; tagIds: string[] }) {\n  const tags = useDocs<Tag>(collection(getFirestore(testApp), \"tags\"), tagIds)\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out @chrisbianca's [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can doa single query and get all the data you need.\n\nIf you want to take this \"denormalized\" approach check out the [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that the synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                  | use-firestore | react-firebase-hooks                |\n| ------------------------------------------------ | ------------- | ----------------------------------- |\n| React-based                                      | ✅            | ✅                                  |\n| Realtime updates                                 | ✅            | ✅                                  |\n| Re-use queries application-wide                  | ✅            | ❌                                  |\n| Throws errors                                    | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                               | ✅            | ❌                                  |\n| Batch document reads to avoid N+1 problem        | ✅            | ❌                                  |\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(collection(getFirestore(app), \"users\"), where(\"teamId\", \"==\", teamId))\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(collection(getFirestore(app), \"tags\"), tagIds)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(query(collection(getFirestore(app), \"stories\")))\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(query(collection(getFirestore(app), \"tags\")))\n  const tagsById = useGlobalMemo(\"tagsById\", () => tags && keyBy(tags), [tags])\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n### Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [ ] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n","readmeFilename":"README.md","gitHead":"e4bb1c86dcd60242a69340bfd0092fe31ea5f046","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.10.0-beta.2","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-S4uXN/3zFzHHq5LpxgsaRuNlQv75VzQkQGJvgy7nUrahOx3602cKyOb6cidkZowmOY4VAei7NfAIBkKS1Jch+w==","shasum":"f5ec9236a729741043dbbb6cf2fde09a89bd6599","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.10.0-beta.2.tgz","fileCount":26,"unpackedSize":322418,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCTJAQriNm+hk/vI4SrlfiJZNLXupX2m1n5Lmoqz9BRWAIhALM1MU8oHBeIX55jApzfL60p+rMpCAypLRZOLcW+baDF"}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.10.0-beta.2_1689881885485_0.7426986841694818"},"_hasShrinkwrap":false},"0.10.0":{"name":"use-firestore","version":"0.10.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","gitHead":"f051ae941b70fedbd60607bd989884900f2013ed","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.10.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-c6RCr69BYnJVggv34IDz+ed2npH15Ozje8A+zG2Grqx1v4LCsH9Lgk2DTlai+bI8K/10RRLtL5N+8lQEilK8Sg==","shasum":"2b6b942a3e9f61a5b5507dc705333dd5dfee50da","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.10.0.tgz","fileCount":47,"unpackedSize":412440,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIB78q3JRDjfMhJau8S7YbvKzQtRDjnoNmf/XoWIg+p8aAiEAyPD9HmA+ash+KYcE0SvKUimUBfV0Q4vRifBlehA1ykM="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.10.0_1689883886554_0.7219267708740933"},"_hasShrinkwrap":false},"0.11.0-beta.0":{"name":"use-firestore","version":"0.11.0-beta.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Example code](#example-code)\n- [Why](#why)\n- [Todo](#todo)\n\n### What it does\n\nIt does this by caching results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `query` object doesn't need to be the same object for this to work, as long as\nits the same path, filters, and conditions it will produce a cache hit.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({ slug, tagIds }: { slug: string; tagIds: string[] }) {\n  const tags = useDocs<Tag>(collection(getFirestore(testApp), \"tags\"), tagIds)\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out @chrisbianca's [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can doa single query and get all the data you need.\n\nIf you want to take this \"denormalized\" approach check out the [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that the synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                  | use-firestore | react-firebase-hooks                |\n| ------------------------------------------------ | ------------- | ----------------------------------- |\n| React-based                                      | ✅            | ✅                                  |\n| Realtime updates                                 | ✅            | ✅                                  |\n| Re-use queries application-wide                  | ✅            | ❌                                  |\n| Throws errors                                    | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                               | ✅            | ❌                                  |\n| Batch document reads to avoid N+1 problem        | ✅            | ❌                                  |\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(collection(getFirestore(app), \"users\"), where(\"teamId\", \"==\", teamId))\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(collection(getFirestore(app), \"tags\"), tagIds)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(query(collection(getFirestore(app), \"stories\")))\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(query(collection(getFirestore(app), \"tags\")))\n  const tagsById = useGlobalMemo(\"tagsById\", () => tags && keyBy(tags), [tags])\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n### Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [ ] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n","readmeFilename":"README.md","gitHead":"c3d10f7cf410f6bc4297b3e28d9b758434e0b795","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.11.0-beta.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-Y7oQyuXv9+t+OM5o3R/1yKZBpXSIyW0wGjm4LI64rMKtw8HVoMLIQmU0hRsq0lvRzZfjIl2Rru/BH8p91SHu7g==","shasum":"867507a9fe7d05d5e06eee9649449eac49615bc3","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.11.0-beta.0.tgz","fileCount":26,"unpackedSize":321844,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIB/mJSBYThrje+CwIEbbJVmGv8EhfmL6Yanu8A6QWIi1AiAvr+qZDKpVph5i3CIfHhR+d0Uwuas2qWN5BhetVmxNHA=="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.11.0-beta.0_1689884063472_0.46168871610519857"},"_hasShrinkwrap":false},"0.11.0-beta.1":{"name":"use-firestore","version":"0.11.0-beta.1","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Example code](#example-code)\n- [Why](#why)\n- [Todo](#todo)\n\n### What it does\n\nIt does this by caching results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `query` object doesn't need to be the same object for this to work, as long as\nits the same path, filters, and conditions it will produce a cache hit.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({ slug, tagIds }: { slug: string; tagIds: string[] }) {\n  const tags = useDocs<Tag>(collection(getFirestore(testApp), \"tags\"), tagIds)\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out @chrisbianca's [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can doa single query and get all the data you need.\n\nIf you want to take this \"denormalized\" approach check out the [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that the synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                  | use-firestore | react-firebase-hooks                |\n| ------------------------------------------------ | ------------- | ----------------------------------- |\n| React-based                                      | ✅            | ✅                                  |\n| Realtime updates                                 | ✅            | ✅                                  |\n| Re-use queries application-wide                  | ✅            | ❌                                  |\n| Throws errors                                    | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                               | ✅            | ❌                                  |\n| Batch document reads to avoid N+1 problem        | ✅            | ❌                                  |\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(collection(getFirestore(app), \"users\"), where(\"teamId\", \"==\", teamId))\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(collection(getFirestore(app), \"tags\"), tagIds)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(query(collection(getFirestore(app), \"stories\")))\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(query(collection(getFirestore(app), \"tags\")))\n  const tagsById = useGlobalMemo(\"tagsById\", () => tags && keyBy(tags), [tags])\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n### Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [ ] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n","readmeFilename":"README.md","gitHead":"c4ab3d197521082d6af0adcc4eae9e818e3661ff","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.11.0-beta.1","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-jkcJuYKg3kTwnCNkspoaEnh58UTb8c5k5PyMJiQ5rQDb+kNfHr+JbesEpmdcamrMT1q4sHxI/QjimJwMUel73g==","shasum":"930709f86855421fd8381151eacc8bdac155c0a5","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.11.0-beta.1.tgz","fileCount":26,"unpackedSize":340529,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDjt3GCnjPW+94PK4IVWICs95vM3jqxXHpc+xThodm4EgIgcsQIa86awVwDjBMcyATP6M9+R86mgpji+Ypct21Inmg="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.11.0-beta.1_1689911591238_0.5705054242264049"},"_hasShrinkwrap":false},"0.11.0-beta.2":{"name":"use-firestore","version":"0.11.0-beta.2","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({ slug, tagIds }: { slug: string; tagIds: string[] }) {\n  const tags = useDocs<Tag>(collection(getFirestore(testApp), \"tags\"), tagIds)\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(collection(getFirestore(app), \"users\"), where(\"teamId\", \"==\", teamId))\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(collection(getFirestore(app), \"tags\"), tagIds)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(collection(getFirestore(app), \"repos\"), \"tagIds\")\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport { deleteDocs, andDeleteAssociatedDocs } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(collection(getFirestore(app), \"highlights\"), \"tagId\")\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n**Warnings**:\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(query(collection(getFirestore(app), \"stories\")))\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(query(collection(getFirestore(app), \"tags\")))\n  const tagsById = useGlobalMemo(\"tagsById\", () => tags && keyBy(tags), [tags])\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n### Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"cef850d64302ade9e4b54a21347280c8fffc07a8","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.11.0-beta.2","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-+GIY+8NAP6ALBKfulUzGv5Po7py8tdNgyXVQJdlITidABch6IyRYywQW21i8/Adxj0Hj4isW1prl4NvGE9NhMg==","shasum":"637072cdcc0f8cc0f0cb6a87ac8bbfbd6ca35a19","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.11.0-beta.2.tgz","fileCount":44,"unpackedSize":455460,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCg6Spkj2PQI3iFaKfdwTBylawCoCBBFRP3XfjfMPCtFgIhAPrX94JkhDmyrWz8oO3f6QfF+aNlFH3fyMUb/x/QzCaD"}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.11.0-beta.2_1690347414129_0.20398070685738845"},"_hasShrinkwrap":false},"0.11.0-beta.3":{"name":"use-firestore","version":"0.11.0-beta.3","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({ slug, tagIds }: { slug: string; tagIds: string[] }) {\n  const tags = useDocs<Tag>(collection(getFirestore(testApp), \"tags\"), tagIds)\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(collection(getFirestore(app), \"users\"), where(\"teamId\", \"==\", teamId))\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(collection(getFirestore(app), \"tags\"), tagIds)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(collection(getFirestore(app), \"repos\"), \"tagIds\")\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport { deleteDocs, andDeleteAssociatedDocs } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(collection(getFirestore(app), \"highlights\"), \"tagId\")\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n**Warnings**:\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(query(collection(getFirestore(app), \"stories\")))\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(query(collection(getFirestore(app), \"tags\")))\n  const tagsById = useGlobalMemo(\"tagsById\", () => tags && keyBy(tags), [tags])\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n### Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"9fbe38e01ed4674fb6f37dee22cbd4f8e8557087","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.11.0-beta.3","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-Tba1FwsYQdBbNg+4DA6Sa3iixd5zeToZzD+3KzrHnh8Wq/TP1H1lTIaM4Kvx5A0aV4vGuSsWrtmUIkov3hFcAA==","shasum":"2294fda2e7a55374c01a4f47d6ea5085fa31e8f9","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.11.0-beta.3.tgz","fileCount":28,"unpackedSize":343748,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCpvhFLGxxQ1LiDh7ve9D5YcLsHyIKVXkmNpoR+ua3l5AIhALy6efuS58zgHPV2QvMTZi7gLNH9dt/IlPg0z76MjiWp"}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.11.0-beta.3_1690347477481_0.2617976920255365"},"_hasShrinkwrap":false},"0.11.0-beta.4":{"name":"use-firestore","version":"0.11.0-beta.4","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({ slug, tagIds }: { slug: string; tagIds: string[] }) {\n  const tags = useDocs<Tag>(collection(getFirestore(testApp), \"tags\"), tagIds)\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ require manual error handling    |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(collection(getFirestore(app), \"users\"), where(\"teamId\", \"==\", teamId))\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(collection(getFirestore(app), \"tags\"), tagIds)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(collection(getFirestore(app), \"repos\"), \"tagIds\")\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport { deleteDocs, andDeleteAssociatedDocs } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(collection(getFirestore(app), \"highlights\"), \"tagId\")\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n**Warnings**:\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(query(collection(getFirestore(app), \"stories\")))\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(query(collection(getFirestore(app), \"tags\")))\n  const tagsById = useGlobalMemo(\"tagsById\", () => tags && keyBy(tags), [tags])\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n### Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"23f0515700879b93931198dd3c97db30e80a06da","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.11.0-beta.4","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-vI3h3nHcPaVVXJZzGlGXX3exEtRtlUwRMI//4HfWFzbhZhBoPIinM0rACw86I3tePMsIWjdu7+b1lXHOu1vUxQ==","shasum":"39e586b7736b29ceafe3b29537c5512b1f26afdf","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.11.0-beta.4.tgz","fileCount":28,"unpackedSize":355195,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIC1nMMRqprjT4MkhGn2DuY5X4SQURLJiQPJiCpPQdV2PAiA8Nyauv1ZpBoEJeHjZsu/QYmqnvVikcSPF0p6RPupJPw=="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.11.0-beta.4_1690347600897_0.38508371476572845"},"_hasShrinkwrap":false},"0.11.0":{"name":"use-firestore","version":"0.11.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","gitHead":"d3f610bfd0763fa7f0be819fc41761a4fa3df1fc","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.11.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-qOJ18UYrV/gyM2vAdepjJpWVK1iIBJnCry4oimKHJXP1f1+JYhB1UhlEX+rO2iuSfaeH+EJrkFqDHuxKyHTB/g==","shasum":"e880eebf666cb68109501c265c96d44b5d3e3923","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.11.0.tgz","fileCount":28,"unpackedSize":355188,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIBYeZQmHmUHnd1wLPuSQWqGVo4HysfrFxvtid0Vsl8RGAiAkjyG0qck9ZDz0QG72AYnUH5s8iGXH9vrPwlFBYv7qBA=="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.11.0_1690437413696_0.6120166975299164"},"_hasShrinkwrap":false},"0.12.0-beta.0":{"name":"use-firestore","version":"0.12.0-beta.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({ slug, tagIds }: { slug: string; tagIds: string[] }) {\n  const tags = useDocs<Tag>(collection(getFirestore(testApp), \"tags\"), tagIds)\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ require manual error handling    |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(collection(getFirestore(app), \"users\"), where(\"teamId\", \"==\", teamId))\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(collection(getFirestore(app), \"tags\"), tagIds)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(collection(getFirestore(app), \"repos\"), \"tagIds\")\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport { deleteDocs, andDeleteAssociatedDocs } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(collection(getFirestore(app), \"highlights\"), \"tagId\")\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n**Warnings**:\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(query(collection(getFirestore(app), \"stories\")))\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(query(collection(getFirestore(app), \"tags\")))\n  const tagsById = useGlobalMemo(\"tagsById\", () => tags && keyBy(tags), [tags])\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n### Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"c4f9db7a9bd98c3c517b0aac7b4546ea72960078","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.12.0-beta.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-oDIAsp58gywYd6FhRgxQjxofmQ6+PC7Ygv/s4NRS5FmcP9r730pnungoD7gp9JETQWh/D+NFanezncSh4hmwrg==","shasum":"21d2819ca14667b9c82e984549119d9b9d16e235","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.12.0-beta.0.tgz","fileCount":28,"unpackedSize":359348,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDChV1cagOdIDzJ47yFHKyqGQnFoFN7keFktgZ/Zq0ceQIhAIzX7L2MRcwUXmKBWXMmgalz+HRJXNOhL0KK7SgqdyWZ"}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.12.0-beta.0_1690559590901_0.7955371252293302"},"_hasShrinkwrap":false},"0.12.0-beta.1":{"name":"use-firestore","version":"0.12.0-beta.1","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n  - [Nested delete](#nested-delete)\n- [Warnings](#warnings)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({\n  slug,\n  tagIds,\n}: {\n  slug: string\n  tagIds: string[]\n}) {\n  const tags = useDocs<Tag>(\n    collection(getFirestore(testApp), \"tags\"),\n    tagIds\n  )\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(\n      collection(getFirestore(app), \"users\"),\n      where(\"teamId\", \"==\", teamId)\n    )\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\nThe `useDocs` hook obviously can be used to fetch multiple documents by ID in a single call....\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(\n  collection(getFirestore(app), \"tags\"),\n  tagIds\n)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n... however it's most useful for efficient querying of 1:N relationships in a tree of React components:\n\n```\n[example needed]\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(\n    collection(getFirestore(app), \"repos\"),\n    \"tagIds\"\n  )\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\"\n  )\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n### Nested **delete**\n\nYou can also go multiple levels deep with your deletions. For example, if every \"highlight\" belongs to a \"tag\" and every \"document\" has many \"highlights\", when you delete a tag you want to:\n\n1. Delete all of the highlights associated with that tag\n2. Remove all of those highlights from any documents they are referenced in\n3. Finally, delete the highlights.\n\nThe code for that would look like:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n  andRemoveFromIds,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [tag.id],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\",\n    andRemoveFromIds(\n      collection(getFirestore(app), \"documents\"),\n      \"highlightIds\"\n    )\n  )\n)\n```\n\n## Warnings\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(\n    query(collection(getFirestore(app), \"stories\"))\n  )\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(\n    query(collection(getFirestore(app), \"tags\"))\n  )\n  const tagsById = useGlobalMemo(\n    \"tagsById\",\n    () => tags && keyBy(tags),\n    [tags]\n  )\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n## Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"66d46683739db8f553c7e9cd62f110f6e23575eb","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.12.0-beta.1","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-I8Pu4+RIXRawcqpSaayDuLqUoXbo69gyzfh7YA4RdM5+Y1nzveKDAMXnnaaAJ4gBtbY9LUN8rtFjJYbXv1i6FA==","shasum":"6c12250f28c420767d1139e4928ded49a3316477","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.12.0-beta.1.tgz","fileCount":28,"unpackedSize":365801,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIBkrTS2o8yDE09ndVpdRJRS1hVV8it4LEYxbq6tcWBjRAiEA6N6shFzko+wakAi1ItYwtW5dVZm8pibQj2uVcQoR03k="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.12.0-beta.1_1690778937652_0.7673024762350948"},"_hasShrinkwrap":false},"0.12.0":{"name":"use-firestore","version":"0.12.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","gitHead":"7abd208d6e4f995479cd1f347d6126340dd9f813","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.12.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-rzkhxMkVtFEFgguRhhUHtKL9690aNHEMlHRGhcZFQ+iSVt2rTcMJh4TaHPoDZo3AXRsdH1YGevhAPuNOjX6AAQ==","shasum":"767b01b4a4cc618deabd5c90d8089b5dcfa4b7f3","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.12.0.tgz","fileCount":28,"unpackedSize":368038,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIH+4ypTvUmvBoNU/MrIcf4Y5LmilQ15hrz0MTgFMXwvSAiEAl8UmDpsGcxkAqNBWrQxGlEbwV0ZvSnBMNV5WRbxBQk4="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.12.0_1690786337874_0.19487862529305122"},"_hasShrinkwrap":false},"0.13.0":{"name":"use-firestore","version":"0.13.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","gitHead":"f55f8af93681f6d780e65eacb60f70e96aea1f88","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.13.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-P6nEryybz2yl1IJOQR2Z9tW85fE6T869AgY/jItOsmkb1eJb/L2Bj5dpYe5bMHlC7PhTaZcwG762r3sn3+i3pg==","shasum":"d3c1461436203ef615129a4fb7525f255af100d6","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.13.0.tgz","fileCount":28,"unpackedSize":367722,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDA+dmF6C8FrUc0OK2oDPFmg+fILSfY3NjvcQuAwcg+DQIhAOjpPMneESbHFrzdjpm4fXkw73orCpbKmqJByDhoyV/i"}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.13.0_1690786471710_0.42504103978294094"},"_hasShrinkwrap":false},"0.13.1-beta.0":{"name":"use-firestore","version":"0.13.1-beta.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n  - [Nested delete](#nested-delete)\n- [Warnings](#warnings)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({\n  slug,\n  tagIds,\n}: {\n  slug: string\n  tagIds: string[]\n}) {\n  const tags = useDocs<Tag>(\n    collection(getFirestore(testApp), \"tags\"),\n    tagIds\n  )\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(\n      collection(getFirestore(app), \"users\"),\n      where(\"teamId\", \"==\", teamId)\n    )\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\nThe `useDocs` hook obviously can be used to fetch multiple documents by ID in a single call....\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(\n  collection(getFirestore(app), \"tags\"),\n  tagIds\n)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n... however it's most useful for efficient querying of 1:N relationships in a tree of React components:\n\n```\n[example needed]\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(\n    collection(getFirestore(app), \"repos\"),\n    \"tagIds\"\n  )\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\"\n  )\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n### Nested **delete**\n\nYou can also go multiple levels deep with your deletions. For example, if every \"highlight\" belongs to a \"tag\" and every \"document\" has many \"highlights\", when you delete a tag you want to:\n\n1. Delete all of the highlights associated with that tag\n2. Remove all of those highlights from any documents they are referenced in\n3. Finally, delete the highlights.\n\nThe code for that would look like:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n  andRemoveFromIds,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [tag.id],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\",\n    andRemoveFromIds(\n      collection(getFirestore(app), \"documents\"),\n      \"highlightIds\"\n    )\n  )\n)\n```\n\n## Warnings\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(\n    query(collection(getFirestore(app), \"stories\"))\n  )\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(\n    query(collection(getFirestore(app), \"tags\"))\n  )\n  const tagsById = useGlobalMemo(\n    \"tagsById\",\n    () => tags && keyBy(tags),\n    [tags]\n  )\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n## Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"d838ab6b3bfd1541afcbb98ea29e1d22deb03577","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.13.1-beta.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-si0DuMLs70uPVDiapqfDjnCvRvjAdLAc828ZWa0jx0arofJZx77ZZ+v4jruLqJJ1Lhruj1pKsuo+TR6k4LXQRA==","shasum":"e3aa0982e3a047b7a10beb7731b4497cdccb0c89","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.13.1-beta.0.tgz","fileCount":28,"unpackedSize":372180,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIEU2GhR4qYBypys6vBtK2kDKU2eCn8/FEUf3BpUY2Pi4AiAcrja7DA3dL090KfHVezmP4MRLDa9oI127Rrvl6IRkAg=="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.13.1-beta.0_1691607183394_0.05626287503048433"},"_hasShrinkwrap":false},"0.14.0-beta.0":{"name":"use-firestore","version":"0.14.0-beta.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n  - [Nested delete](#nested-delete)\n- [Warnings](#warnings)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({\n  slug,\n  tagIds,\n}: {\n  slug: string\n  tagIds: string[]\n}) {\n  const tags = useDocs<Tag>(\n    collection(getFirestore(testApp), \"tags\"),\n    tagIds\n  )\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(\n      collection(getFirestore(app), \"users\"),\n      where(\"teamId\", \"==\", teamId)\n    )\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\nThe `useDocs` hook obviously can be used to fetch multiple documents by ID in a single call....\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(\n  collection(getFirestore(app), \"tags\"),\n  tagIds\n)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n... however it's most useful for efficient querying of 1:N relationships in a tree of React components:\n\n```\n[example needed]\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(\n    collection(getFirestore(app), \"repos\"),\n    \"tagIds\"\n  )\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\"\n  )\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n### Nested **delete**\n\nYou can also go multiple levels deep with your deletions. For example, if every \"highlight\" belongs to a \"tag\" and every \"document\" has many \"highlights\", when you delete a tag you want to:\n\n1. Delete all of the highlights associated with that tag\n2. Remove all of those highlights from any documents they are referenced in\n3. Finally, delete the highlights.\n\nThe code for that would look like:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n  andRemoveFromIds,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [tag.id],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\",\n    andRemoveFromIds(\n      collection(getFirestore(app), \"documents\"),\n      \"highlightIds\"\n    )\n  )\n)\n```\n\n## Warnings\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(\n    query(collection(getFirestore(app), \"stories\"))\n  )\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(\n    query(collection(getFirestore(app), \"tags\"))\n  )\n  const tagsById = useGlobalMemo(\n    \"tagsById\",\n    () => tags && keyBy(tags),\n    [tags]\n  )\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n## Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"134afcc7e1328165496825f9d0285876dc6e4a72","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.14.0-beta.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-aaYfZkvj3yHEikZtY6QzrZOhA2MO45SJ6iVfGDpkVxZ1hMkYWQmHrPkte9Zhm4/PyHpGzbxMrf8L5kaa9RYfIA==","shasum":"7097a56d455b048b3354a1cc4bee50b7e67350a4","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.14.0-beta.0.tgz","fileCount":28,"unpackedSize":372180,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIDKzXeiccvgScbd/xq9EB055RJnpNZGFxyBKKwVT2J0qAiEAhbToVF4ERcYJY1REW6ROxNd+N0KP+c4NcdiTn1crbDE="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.14.0-beta.0_1691607235751_0.10650276179404705"},"_hasShrinkwrap":false},"0.14.0-beta.1":{"name":"use-firestore","version":"0.14.0-beta.1","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n  - [Nested delete](#nested-delete)\n- [Warnings](#warnings)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({\n  slug,\n  tagIds,\n}: {\n  slug: string\n  tagIds: string[]\n}) {\n  const tags = useDocs<Tag>(\n    collection(getFirestore(testApp), \"tags\"),\n    tagIds\n  )\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(\n      collection(getFirestore(app), \"users\"),\n      where(\"teamId\", \"==\", teamId)\n    )\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\nThe `useDocs` hook obviously can be used to fetch multiple documents by ID in a single call....\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(\n  collection(getFirestore(app), \"tags\"),\n  tagIds\n)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n... however it's most useful for efficient querying of 1:N relationships in a tree of React components:\n\n```\n[example needed]\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(\n    collection(getFirestore(app), \"repos\"),\n    \"tagIds\"\n  )\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\"\n  )\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n### Nested **delete**\n\nYou can also go multiple levels deep with your deletions. For example, if every \"highlight\" belongs to a \"tag\" and every \"document\" has many \"highlights\", when you delete a tag you want to:\n\n1. Delete all of the highlights associated with that tag\n2. Remove all of those highlights from any documents they are referenced in\n3. Finally, delete the highlights.\n\nThe code for that would look like:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n  andRemoveFromIds,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [tag.id],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\",\n    andRemoveFromIds(\n      collection(getFirestore(app), \"documents\"),\n      \"highlightIds\"\n    )\n  )\n)\n```\n\n## Warnings\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(\n    query(collection(getFirestore(app), \"stories\"))\n  )\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(\n    query(collection(getFirestore(app), \"tags\"))\n  )\n  const tagsById = useGlobalMemo(\n    \"tagsById\",\n    () => tags && keyBy(tags),\n    [tags]\n  )\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n## Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"03bd22879c187075992c9c2dcafa37464177b7cc","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.14.0-beta.1","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-6qM9fu9l+7x/uMmffPRiBo75w5S+4E2XM3QlHvsTNFfContPWXqOXSUNWGzjOwl2gpctmtlrZtCoqOgBBzBuRg==","shasum":"e1cd6da493b3d978c58785ef9e694d5289f3de7c","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.14.0-beta.1.tgz","fileCount":28,"unpackedSize":371034,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCHuahC3EJWjLxBQELskJus2t3EunQfdwIS52086l14PQIgW0Y+94WXAf01KCa3GHbmD6ufgXvsVn/3E6oCHwkY40Q="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.14.0-beta.1_1691608620595_0.5099836992690396"},"_hasShrinkwrap":false},"0.14.0":{"name":"use-firestore","version":"0.14.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","gitHead":"bbfad635a6948cfe559f8f151e16383c212881a8","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.14.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-gHWep3+j5avcQUsnMMFLwWq3/A3YHGXlFc6WEgwzThRX4/hgb/mFXqX7QqnKyJY89P86Yx3a3dmeu2m04HzKUw==","shasum":"65350ccbc6ba4311e20af049ae1f5d159c5d9f87","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.14.0.tgz","fileCount":28,"unpackedSize":373739,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIC3XASpKJfO5FTUsjHXKyRF51aOawOHslE4U897SQHuiAiAa8koI8DhamKgr7T++srxSYbw3nPEEaaDoPySqKqvbug=="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.14.0_1691611929427_0.5960439244967994"},"_hasShrinkwrap":false},"0.15.0-beta.0":{"name":"use-firestore","version":"0.15.0-beta.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n  - [Nested delete](#nested-delete)\n- [Warnings](#warnings)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({\n  slug,\n  tagIds,\n}: {\n  slug: string\n  tagIds: string[]\n}) {\n  const tags = useDocs<Tag>(\n    collection(getFirestore(testApp), \"tags\"),\n    tagIds\n  )\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(\n      collection(getFirestore(app), \"users\"),\n      where(\"teamId\", \"==\", teamId)\n    )\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\nThe `useDocs` hook obviously can be used to fetch multiple documents by ID in a single call....\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(\n  collection(getFirestore(app), \"tags\"),\n  tagIds\n)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n... however it's most useful for efficient querying of 1:N relationships in a tree of React components:\n\n```\n[example needed]\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(\n    collection(getFirestore(app), \"repos\"),\n    \"tagIds\"\n  )\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\"\n  )\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n### Nested **delete**\n\nYou can also go multiple levels deep with your deletions. For example, if every \"highlight\" belongs to a \"tag\" and every \"document\" has many \"highlights\", when you delete a tag you want to:\n\n1. Delete all of the highlights associated with that tag\n2. Remove all of those highlights from any documents they are referenced in\n3. Finally, delete the highlights.\n\nThe code for that would look like:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n  andRemoveFromIds,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [tag.id],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\",\n    andRemoveFromIds(\n      collection(getFirestore(app), \"documents\"),\n      \"highlightIds\"\n    )\n  )\n)\n```\n\n## Warnings\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(\n    query(collection(getFirestore(app), \"stories\"))\n  )\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(\n    query(collection(getFirestore(app), \"tags\"))\n  )\n  const tagsById = useGlobalMemo(\n    \"tagsById\",\n    () => tags && keyBy(tags),\n    [tags]\n  )\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n## Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"078f7a205dcb0735f5dac5515fc2b78f4f491062","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.15.0-beta.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-8+2exX0BDKrLAvSthODZBFKAxoOEOWv2hSWCkImg+tncm9p4MT/pNcKS6V7WROC6qPiYH3X3vo0VAkNbf+NXmA==","shasum":"011a3c76f8d14db6f8351eb5f7df4d1d98e2a1fc","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.15.0-beta.0.tgz","fileCount":28,"unpackedSize":374015,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIDieBguErXFMH9ng5k2NukSEdUqGOaGSeznoN6yx2fKoAiEA/q04Zw8esRTwxZqiZL06sKrUhrBbhPDkgOP8HfqEy3A="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.15.0-beta.0_1691645386544_0.5126686674353775"},"_hasShrinkwrap":false},"0.15.0":{"name":"use-firestore","version":"0.15.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","gitHead":"b4d093debb12998938dd7e438371603e614fe2cb","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.15.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-lE4j5oEGtsjxxshmeTZM1m5HxmTfZ6reEpwrfg9PrWGOOPNU6o0efiwXV0idCgjdC5Heqn1Y+M1HUHw6hk+pzA==","shasum":"ac5a2c0cc618946f20126e81b17a74975626dab5","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.15.0.tgz","fileCount":28,"unpackedSize":374008,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIE/9ifnyq+o0dGoTCl1GFffDrXMAUR2aBZpCHKePHBXzAiEA07YstwZwpxSMs2oZsrr0MvB40txT1sXOv9lNFszYF78="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.15.0_1691646538255_0.5290228309733882"},"_hasShrinkwrap":false},"0.16.0-beta.0":{"name":"use-firestore","version":"0.16.0-beta.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n  - [Nested delete](#nested-delete)\n- [Warnings](#warnings)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({\n  slug,\n  tagIds,\n}: {\n  slug: string\n  tagIds: string[]\n}) {\n  const tags = useDocs<Tag>(\n    collection(getFirestore(testApp), \"tags\"),\n    tagIds\n  )\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(\n      collection(getFirestore(app), \"users\"),\n      where(\"teamId\", \"==\", teamId)\n    )\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\nThe `useDocs` hook obviously can be used to fetch multiple documents by ID in a single call....\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(\n  collection(getFirestore(app), \"tags\"),\n  tagIds\n)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n... however it's most useful for efficient querying of 1:N relationships in a tree of React components:\n\n```\n[example needed]\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(\n    collection(getFirestore(app), \"repos\"),\n    \"tagIds\"\n  )\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\"\n  )\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n### Nested **delete**\n\nYou can also go multiple levels deep with your deletions. For example, if every \"highlight\" belongs to a \"tag\" and every \"document\" has many \"highlights\", when you delete a tag you want to:\n\n1. Delete all of the highlights associated with that tag\n2. Remove all of those highlights from any documents they are referenced in\n3. Finally, delete the highlights.\n\nThe code for that would look like:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n  andRemoveFromIds,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [tag.id],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\",\n    andRemoveFromIds(\n      collection(getFirestore(app), \"documents\"),\n      \"highlightIds\"\n    )\n  )\n)\n```\n\n## Warnings\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(\n    query(collection(getFirestore(app), \"stories\"))\n  )\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(\n    query(collection(getFirestore(app), \"tags\"))\n  )\n  const tagsById = useGlobalMemo(\n    \"tagsById\",\n    () => tags && keyBy(tags),\n    [tags]\n  )\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n## Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"4b54aa948e68f4598c7cabbf386f29c3e02b54da","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.16.0-beta.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-1tWQjJlorKOVLkCWneFVpeSmJFon2OoDe5tcoxPa2WjeP+HRgE0NQ9LKYPUojYUJXANosVYrHtKnEn+ml6cJKQ==","shasum":"1c8f6255105ccd69b5f7aed5b1e4415effa0576f","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.16.0-beta.0.tgz","fileCount":33,"unpackedSize":412781,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIGS8nXVodzhWfyQ1xSkWHN6XnQmBdjdcyjSB9nVx6EvuAiEAuDeK84zELVo575sc0K73fIIb3HqNzGnrD57FcMJbu6s="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.16.0-beta.0_1692980274685_0.6094285042170162"},"_hasShrinkwrap":false},"0.17.0-beta.0":{"name":"use-firestore","version":"0.17.0-beta.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n  - [Nested delete](#nested-delete)\n- [Warnings](#warnings)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({\n  slug,\n  tagIds,\n}: {\n  slug: string\n  tagIds: string[]\n}) {\n  const tags = useDocs<Tag>(\n    collection(getFirestore(testApp), \"tags\"),\n    tagIds\n  )\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(\n      collection(getFirestore(app), \"users\"),\n      where(\"teamId\", \"==\", teamId)\n    )\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\nThe `useDocs` hook obviously can be used to fetch multiple documents by ID in a single call....\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(\n  collection(getFirestore(app), \"tags\"),\n  tagIds\n)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n... however it's most useful for efficient querying of 1:N relationships in a tree of React components:\n\n```\n[example needed]\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(\n    collection(getFirestore(app), \"repos\"),\n    \"tagIds\"\n  )\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\"\n  )\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n### Nested **delete**\n\nYou can also go multiple levels deep with your deletions. For example, if every \"highlight\" belongs to a \"tag\" and every \"document\" has many \"highlights\", when you delete a tag you want to:\n\n1. Delete all of the highlights associated with that tag\n2. Remove all of those highlights from any documents they are referenced in\n3. Finally, delete the highlights.\n\nThe code for that would look like:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n  andRemoveFromIds,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [tag.id],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\",\n    andRemoveFromIds(\n      collection(getFirestore(app), \"documents\"),\n      \"highlightIds\"\n    )\n  )\n)\n```\n\n## Warnings\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(\n    query(collection(getFirestore(app), \"stories\"))\n  )\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(\n    query(collection(getFirestore(app), \"tags\"))\n  )\n  const tagsById = useGlobalMemo(\n    \"tagsById\",\n    () => tags && keyBy(tags),\n    [tags]\n  )\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n## Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"55e5d6b8c2f5a37d9de6f9d066790e0622e13c94","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.17.0-beta.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-NRlgyp9yZKVd6bNLFfaKrLW1qu+rHQiQsOlhP6sB1R9nkWO/Gd3D9zQ9eDcAkNJE3mCpZE9GjhANCXmGulCLKA==","shasum":"432210671c173f01f9f8bff1b08265485c0b54d9","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.17.0-beta.0.tgz","fileCount":33,"unpackedSize":413482,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIFgrAmUhQ75K6jYyAXKieclPW3wHCiFYTp0/gu8VADqlAiA+9w9pbbeTTDfBpMohCkG266SmngaUy4RURMDGdz8PNw=="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.17.0-beta.0_1693027438645_0.7200119936768943"},"_hasShrinkwrap":false},"0.17.0-beta.1":{"name":"use-firestore","version":"0.17.0-beta.1","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n  - [Nested delete](#nested-delete)\n- [Warnings](#warnings)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({\n  slug,\n  tagIds,\n}: {\n  slug: string\n  tagIds: string[]\n}) {\n  const tags = useDocs<Tag>(\n    collection(getFirestore(testApp), \"tags\"),\n    tagIds\n  )\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(\n      collection(getFirestore(app), \"users\"),\n      where(\"teamId\", \"==\", teamId)\n    )\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\nThe `useDocs` hook obviously can be used to fetch multiple documents by ID in a single call....\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(\n  collection(getFirestore(app), \"tags\"),\n  tagIds\n)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n... however it's most useful for efficient querying of 1:N relationships in a tree of React components:\n\n```\n[example needed]\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(\n    collection(getFirestore(app), \"repos\"),\n    \"tagIds\"\n  )\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\"\n  )\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n### Nested **delete**\n\nYou can also go multiple levels deep with your deletions. For example, if every \"highlight\" belongs to a \"tag\" and every \"document\" has many \"highlights\", when you delete a tag you want to:\n\n1. Delete all of the highlights associated with that tag\n2. Remove all of those highlights from any documents they are referenced in\n3. Finally, delete the highlights.\n\nThe code for that would look like:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n  andRemoveFromIds,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [tag.id],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\",\n    andRemoveFromIds(\n      collection(getFirestore(app), \"documents\"),\n      \"highlightIds\"\n    )\n  )\n)\n```\n\n## Warnings\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(\n    query(collection(getFirestore(app), \"stories\"))\n  )\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(\n    query(collection(getFirestore(app), \"tags\"))\n  )\n  const tagsById = useGlobalMemo(\n    \"tagsById\",\n    () => tags && keyBy(tags),\n    [tags]\n  )\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n## Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"d2e2fb020e304584ea5c11e62d45d0cc6ae2fb82","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.17.0-beta.1","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-jCzmUZgrRZm7N8ds2Baj86iYOL74DPD+aQuZgvcVYvJOQaJnbuSQbDMK61A86G999xl2X4LjrnS1oZLzR0lQmQ==","shasum":"56481c564db38c57681c4e157e9ac9772ab64c61","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.17.0-beta.1.tgz","fileCount":33,"unpackedSize":414625,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQD2PjqhGn0iPHK3ZwmHe4zoymtvLwrr0t0jTFdnI8spzQIhAMaf9mtOB6gmMtKUo6qy+i0MzPg3kJfWVYU79SeC+iRu"}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.17.0-beta.1_1693063244654_0.07464356879959144"},"_hasShrinkwrap":false},"0.17.0-beta.2":{"name":"use-firestore","version":"0.17.0-beta.2","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n  - [Nested delete](#nested-delete)\n- [Warnings](#warnings)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({\n  slug,\n  tagIds,\n}: {\n  slug: string\n  tagIds: string[]\n}) {\n  const tags = useDocs<Tag>(\n    collection(getFirestore(testApp), \"tags\"),\n    tagIds\n  )\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(\n      collection(getFirestore(app), \"users\"),\n      where(\"teamId\", \"==\", teamId)\n    )\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\nThe `useDocs` hook obviously can be used to fetch multiple documents by ID in a single call....\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(\n  collection(getFirestore(app), \"tags\"),\n  tagIds\n)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n... however it's most useful for efficient querying of 1:N relationships in a tree of React components:\n\n```\n[example needed]\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(\n    collection(getFirestore(app), \"repos\"),\n    \"tagIds\"\n  )\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\"\n  )\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n### Nested **delete**\n\nYou can also go multiple levels deep with your deletions. For example, if every \"highlight\" belongs to a \"tag\" and every \"document\" has many \"highlights\", when you delete a tag you want to:\n\n1. Delete all of the highlights associated with that tag\n2. Remove all of those highlights from any documents they are referenced in\n3. Finally, delete the highlights.\n\nThe code for that would look like:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n  andRemoveFromIds,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [tag.id],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\",\n    andRemoveFromIds(\n      collection(getFirestore(app), \"documents\"),\n      \"highlightIds\"\n    )\n  )\n)\n```\n\n## Warnings\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(\n    query(collection(getFirestore(app), \"stories\"))\n  )\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(\n    query(collection(getFirestore(app), \"tags\"))\n  )\n  const tagsById = useGlobalMemo(\n    \"tagsById\",\n    () => tags && keyBy(tags),\n    [tags]\n  )\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n## Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"702d52e1466ec3be3315cded29d1d4f426092093","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.17.0-beta.2","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-G0LZU8nWHGXGwD6guA5VcD+bFsAcyMUnuSSAAYGuTCDB7rpYFOoMtP9XacGPdpDPAwcnLviIo8hYrYPyG9bkrA==","shasum":"801b96e40935d61286bbe6005c508d1b10ebd98c","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.17.0-beta.2.tgz","fileCount":33,"unpackedSize":414702,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCXbxfyJVCrJ1ZXM4gvH3LRGLQS7ZA23ZNr3LooXFP0sAIhAN147WPSHOn7vJjpbnMZMzKzAnZq7aX6IBPt9DnxXulJ"}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.17.0-beta.2_1693071363502_0.7275307811257872"},"_hasShrinkwrap":false},"0.17.0-beta.3":{"name":"use-firestore","version":"0.17.0-beta.3","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n  - [Nested delete](#nested-delete)\n- [Warnings](#warnings)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({\n  slug,\n  tagIds,\n}: {\n  slug: string\n  tagIds: string[]\n}) {\n  const tags = useDocs<Tag>(\n    collection(getFirestore(testApp), \"tags\"),\n    tagIds\n  )\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(\n      collection(getFirestore(app), \"users\"),\n      where(\"teamId\", \"==\", teamId)\n    )\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\nThe `useDocs` hook obviously can be used to fetch multiple documents by ID in a single call....\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(\n  collection(getFirestore(app), \"tags\"),\n  tagIds\n)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n... however it's most useful for efficient querying of 1:N relationships in a tree of React components:\n\n```\n[example needed]\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(\n    collection(getFirestore(app), \"repos\"),\n    \"tagIds\"\n  )\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\"\n  )\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n### Nested **delete**\n\nYou can also go multiple levels deep with your deletions. For example, if every \"highlight\" belongs to a \"tag\" and every \"document\" has many \"highlights\", when you delete a tag you want to:\n\n1. Delete all of the highlights associated with that tag\n2. Remove all of those highlights from any documents they are referenced in\n3. Finally, delete the highlights.\n\nThe code for that would look like:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n  andRemoveFromIds,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [tag.id],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\",\n    andRemoveFromIds(\n      collection(getFirestore(app), \"documents\"),\n      \"highlightIds\"\n    )\n  )\n)\n```\n\n## Warnings\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(\n    query(collection(getFirestore(app), \"stories\"))\n  )\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(\n    query(collection(getFirestore(app), \"tags\"))\n  )\n  const tagsById = useGlobalMemo(\n    \"tagsById\",\n    () => tags && keyBy(tags),\n    [tags]\n  )\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n## Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"10929e5ee9d3c2c4b8b6b65239a929a1e0fad55b","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.17.0-beta.3","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-nJPmKJOllInukds98+W560GahHaY15RbdOsbNVNCdl/QuRnUJkrzUvlcVvvG0l9y8YsIp9hIFRK8+PLsFGv5SA==","shasum":"4996f623b936a7e4d4675cb8152ce4263cc6b648","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.17.0-beta.3.tgz","fileCount":33,"unpackedSize":416741,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDb7ZNchXMnL2CMokEIAQqVW8vVA/GU/t3QkfXm/cMJbgIhAM4mCLRjs9SdqHXzIJK5oNVdgAGswjzSw4Dr08aLPSwW"}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.17.0-beta.3_1693074106654_0.38365216066729424"},"_hasShrinkwrap":false},"0.17.0":{"name":"use-firestore","version":"0.17.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","gitHead":"b6a58189b11b24c95c9d3e00742c6de4158dcf4e","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.17.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-gxRlb+Lf1TM1w4acO0y/m14BPdT7Eet930wx/syCECbqAYgbmmRKo5szaDBJkGQa5s/RgqSleGR948GOp+WaTw==","shasum":"a490aa50e8e6f80463d92eacb9d9175c01b51cea","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.17.0.tgz","fileCount":33,"unpackedSize":416460,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIFHSBJBNJEtzfhGzWFXs7YsDp9fE1pcFCqgXptMcF+6aAiEArtQ1TGcFGxx1hqru9oMkrF7Oyzm2jhKC/En3rCR9dHQ="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.17.0_1693077939989_0.05391907151956765"},"_hasShrinkwrap":false},"0.18.0-beta.0":{"name":"use-firestore","version":"0.18.0-beta.0","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n  - [Nested delete](#nested-delete)\n- [Warnings](#warnings)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({\n  slug,\n  tagIds,\n}: {\n  slug: string\n  tagIds: string[]\n}) {\n  const tags = useDocs<Tag>(\n    collection(getFirestore(testApp), \"tags\"),\n    tagIds\n  )\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(\n      collection(getFirestore(app), \"users\"),\n      where(\"teamId\", \"==\", teamId)\n    )\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\nThe `useDocs` hook obviously can be used to fetch multiple documents by ID in a single call....\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(\n  collection(getFirestore(app), \"tags\"),\n  tagIds\n)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n... however it's most useful for efficient querying of 1:N relationships in a tree of React components:\n\n```\n[example needed]\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(\n    collection(getFirestore(app), \"repos\"),\n    \"tagIds\"\n  )\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\"\n  )\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n### Nested **delete**\n\nYou can also go multiple levels deep with your deletions. For example, if every \"highlight\" belongs to a \"tag\" and every \"document\" has many \"highlights\", when you delete a tag you want to:\n\n1. Delete all of the highlights associated with that tag\n2. Remove all of those highlights from any documents they are referenced in\n3. Finally, delete the highlights.\n\nThe code for that would look like:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n  andRemoveFromIds,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [tag.id],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\",\n    andRemoveFromIds(\n      collection(getFirestore(app), \"documents\"),\n      \"highlightIds\"\n    )\n  )\n)\n```\n\n## Warnings\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(\n    query(collection(getFirestore(app), \"stories\"))\n  )\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(\n    query(collection(getFirestore(app), \"tags\"))\n  )\n  const tagsById = useGlobalMemo(\n    \"tagsById\",\n    () => tags && keyBy(tags),\n    [tags]\n  )\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n## Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"c82ede7eaad96766ca88abc6c318245a503b816c","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.18.0-beta.0","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-SjQSNXHxw1apIiBjKf1Ld0HdkB9eCQdJi6jCv6mlgaAMPXRbZC5JC0rem/A7XGv3jrLvOedA4DTp/IXPAzwnig==","shasum":"12ec29c3e725ac67d712b75eb640574d899d231f","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.18.0-beta.0.tgz","fileCount":33,"unpackedSize":424251,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIGo4RzySFBPZoFhIzp2TjwZHPOfQR6U2AHxF9zNFoJ5iAiABCgYrnRl1jT0zS4toZpGmAaIYch9yM9TJdxv7fbiRBg=="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.18.0-beta.0_1694321719167_0.1468045213122029"},"_hasShrinkwrap":false},"0.18.0-beta.1":{"name":"use-firestore","version":"0.18.0-beta.1","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n  - [Nested delete](#nested-delete)\n- [Warnings](#warnings)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({\n  slug,\n  tagIds,\n}: {\n  slug: string\n  tagIds: string[]\n}) {\n  const tags = useDocs<Tag>(\n    collection(getFirestore(testApp), \"tags\"),\n    tagIds\n  )\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(\n      collection(getFirestore(app), \"users\"),\n      where(\"teamId\", \"==\", teamId)\n    )\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\nThe `useDocs` hook obviously can be used to fetch multiple documents by ID in a single call....\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(\n  collection(getFirestore(app), \"tags\"),\n  tagIds\n)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n... however it's most useful for efficient querying of 1:N relationships in a tree of React components:\n\n```\n[example needed]\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(\n    collection(getFirestore(app), \"repos\"),\n    \"tagIds\"\n  )\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\"\n  )\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n### Nested **delete**\n\nYou can also go multiple levels deep with your deletions. For example, if every \"highlight\" belongs to a \"tag\" and every \"document\" has many \"highlights\", when you delete a tag you want to:\n\n1. Delete all of the highlights associated with that tag\n2. Remove all of those highlights from any documents they are referenced in\n3. Finally, delete the highlights.\n\nThe code for that would look like:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n  andRemoveFromIds,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [tag.id],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\",\n    andRemoveFromIds(\n      collection(getFirestore(app), \"documents\"),\n      \"highlightIds\"\n    )\n  )\n)\n```\n\n## Warnings\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(\n    query(collection(getFirestore(app), \"stories\"))\n  )\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(\n    query(collection(getFirestore(app), \"tags\"))\n  )\n  const tagsById = useGlobalMemo(\n    \"tagsById\",\n    () => tags && keyBy(tags),\n    [tags]\n  )\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n## Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"5af99849190e5b9c459dbd12fa2f11dcda1c54a8","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.18.0-beta.1","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-q38OZB+5Rje3WeUkxAwskMQoNlaeuh76j+3UsMMOlhBncHqvm9M4tjae+Cii4oss/+SGgX2vVL46uzhMqXUYyQ==","shasum":"87b261b1ed34efb1e7991f4072bc0f9ea68ac2de","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.18.0-beta.1.tgz","fileCount":33,"unpackedSize":427892,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDQf0XSLzdIeXmJaByGg4EwVhLrkQJe4veatAz2WqiNpwIgPTJEYcS2mFZY13uxbviPVZVceJuhJbZkSZDWqI9ZWx0="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.18.0-beta.1_1695102774832_0.6778765626974699"},"_hasShrinkwrap":false},"0.18.0-beta.2":{"name":"use-firestore","version":"0.18.0-beta.2","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n  - [Nested delete](#nested-delete)\n- [Warnings](#warnings)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({\n  slug,\n  tagIds,\n}: {\n  slug: string\n  tagIds: string[]\n}) {\n  const tags = useDocs<Tag>(\n    collection(getFirestore(testApp), \"tags\"),\n    tagIds\n  )\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(\n      collection(getFirestore(app), \"users\"),\n      where(\"teamId\", \"==\", teamId)\n    )\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\nThe `useDocs` hook obviously can be used to fetch multiple documents by ID in a single call....\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(\n  collection(getFirestore(app), \"tags\"),\n  tagIds\n)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n... however it's most useful for efficient querying of 1:N relationships in a tree of React components:\n\n```\n[example needed]\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(\n    collection(getFirestore(app), \"repos\"),\n    \"tagIds\"\n  )\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\"\n  )\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n### Nested **delete**\n\nYou can also go multiple levels deep with your deletions. For example, if every \"highlight\" belongs to a \"tag\" and every \"document\" has many \"highlights\", when you delete a tag you want to:\n\n1. Delete all of the highlights associated with that tag\n2. Remove all of those highlights from any documents they are referenced in\n3. Finally, delete the highlights.\n\nThe code for that would look like:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n  andRemoveFromIds,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [tag.id],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\",\n    andRemoveFromIds(\n      collection(getFirestore(app), \"documents\"),\n      \"highlightIds\"\n    )\n  )\n)\n```\n\n## Warnings\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(\n    query(collection(getFirestore(app), \"stories\"))\n  )\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(\n    query(collection(getFirestore(app), \"tags\"))\n  )\n  const tagsById = useGlobalMemo(\n    \"tagsById\",\n    () => tags && keyBy(tags),\n    [tags]\n  )\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n## Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"f93243941be498734933be5e3690ef2e136b34c2","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.18.0-beta.2","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-8X4HtHkawUkcCcoQDph4XhWuxzRMiTjkvCF57g9MJhVbndaxfRD5VZlCnsO6F/C8L4ITP81fnTYx6adXWRXIeQ==","shasum":"975cc0a73338dfa9f4892ea16517e1e62539b037","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.18.0-beta.2.tgz","fileCount":33,"unpackedSize":427587,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIDIQ7iU5g1brITkldMpqxykOn+WuueWvo2W4X6a7K//fAiEAzHfBtPY3NKTftDHumzc+7+zNzRmyWvyLVf3LunA4pkU="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.18.0-beta.2_1695138779320_0.45473724993686804"},"_hasShrinkwrap":false},"0.18.0-beta.3":{"name":"use-firestore","version":"0.18.0-beta.3","license":"MIT","main":"./dist/lib.umd.js","module":"./dist/lib.es.js","exports":{".":{"import":"./dist/lib.es.js","require":"./dist/lib.umd.js"}},"peerDependencies":{"firebase":"^9.22.0","react":"^17.0.0"},"scripts":{"all":"yarn && yarn build && yarn fix && yarn check:types && yarn test && echo `echo 8J+OiSBEaWQgYWxs | base64 -d`","build":"rm -rf dist/* && yarn build:lib && yarn build:types","build:docs":"vite build --config vite.docs.config.js --mode development && mv site/docs/index.html site && rmdir site/docs && cp site/index.html site/404.html","build:lib":"vite build --config vite.lib.config.js --mode development","build:types":"tsc --declaration --emitDeclarationOnly -p tsconfig.dist.json --skipLibCheck && tsc-alias -p tsconfig.json && mv dist/index.d.ts dist/lib.umd.d.ts","check:format":"prettier --check --ignore-path .gitignore .","check:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern .; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","check:types":"tsc --noEmit -p tsconfig.json; if [ $? -eq 0 ]; then echo 8J+OiSBUeXBlcyBhcmUgZ29vZCEKCg== | base64 -d; else exit 1; fi","confgen":"npx confgen@latest @lib @docs --name FirestoreHooks dist:lib git vite react typescript prettier eslint vitest codedocs yarn codespaces githubActions","fix":"yarn fix:lint && yarn fix:format","fix:format":"prettier --write --ignore-path .gitignore .","fix:lint":"eslint --ignore-path .gitignore --no-error-on-unmatched-pattern . --fix; if [ $? -eq 0 ]; then echo 8J+OiSBObyBsaW50IGluIHRoaXMgY29kZSEKCg== | base64 -d; else exit 1; fi","start:docs:dev":"vite serve docs --config vite.docs.config.js","start:emulators":"firebase emulators:start","test":"vitest run --config vite.test.config.js","test:ci":"firebase emulators:exec \"vitest run --config vite.test.config.js\"","test:watch":"vitest watch --config vite.test.config.js"},"packageManager":"yarn@1.22.19","types":"./dist/lib.umd.d.ts","readme":"<p align=\"center\">\n<img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" />\n</p>\n\n<b>use-firestore</b> provides a set of React hooks which let you load Firestore\ndata at the component level.\n\n**Table of Contents**\n\n- [What it does](#what-it-does)\n- [Alternatives](#alternatives)\n- [API Reference](#api-reference)\n  - [`useQuery` hook](#usequery-hook)\n  - [`useDoc` hook with optimistic updates](#usedoc-hook-with-optimistic-updates)\n  - [`useDocs` hook](#usedocs-hook)\n  - [`deleteDocs` function](#deletedocs-function)\n  - [Nested delete](#nested-delete)\n- [Warnings](#warnings)\n- [Why](#why)\n- [Todo](#todo)\n\n## What it does\n\nThe `useQuery`, hook caches results on a per-query basis, such that you can call\nthe same hook with the same query 50 times on the same page, and\n`use-firestore` will only create one single subscription, and will return the\nexact same object or array of objects to all 50 of those hooks.\n\nThe `QueryReference` object that you pass in doesn't even need to be the same\nobject for this to work, as long as it has the same path, filters, and\nconditions it will produce a cache hit.\n\nThe `useDoc` and `useDocs` hooks cache results on a per-collection basis, and\ncreate only one subscription per collection.\n\nThe returned documents will be normal JavaScript objects like:\n\n```js\n{\n  id: \"[document id string]\",\n  field1: value2,\n  field2: value2,\n  ...etc\n}\n```\n\nYou can provide a type assertion as well:\n\n```ts\nconst users = useQuery<Users>(query)\n```\n\nA subscription to Firestore will be created for each unique query, and the\nresults of the hook will be updated in realtime.\n\nLastly, `use-firestore` provides `useDocs` hook which batches collection subscriptions globally, which allows you to fetch associated documents deep in your React Component tree without triggering the N+1 problem.\n\nFor example, if you wanted to query a collection, and then grab associated tags off each document in the result set, this would only require two subscriptions to your Firestore database:\n\n```tsx\nfunction ListRepos({ ownerId }: ListReposProps) {\n  const repos = useQuery<Repo>(\n    query(\n      collection(getFirestore(testApp), \"repos\"),\n      where(\"ownerId\", \"==\", ownerId)\n    )\n  )\n\n  if (!repos) return null\n\n  return (\n    <>\n      {repos.map(({ id, slug, tagIds }) => (\n        <Repo key={id} slug={slug} tagIds={tagIds} />\n      ))}\n    </>\n  )\n}\n\nfunction Repo({\n  slug,\n  tagIds,\n}: {\n  slug: string\n  tagIds: string[]\n}) {\n  const tags = useDocs<Tag>(\n    collection(getFirestore(testApp), \"tags\"),\n    tagIds\n  )\n\n  if (!tags) return null\n\n  return (\n    <li>\n      {slug}\n      {tags.map((tag) => (\n        <span key={tag.id} className={`tag-${tag.color}`}>\n          {tag.text}\n        </span>\n      ))}\n    </li>\n  )\n}\n```\n\n## Alternatives\n\nFor an alternative approach, check out [Chris Bianca's](@chrisbianca) [react-firebase-hooks](https://www.npmjs.com/package/react-firebase-hooks). It's an awesome package that I've used in many projects and Chris is a fantastic developer and maintainer. `react-firebase-hooks` is oriented more towards the \"denomalized\" architecture used in many Firestore projects, where you copy associated data onto your documents so you can get a sub-graph of associated documents in a single database read.\n\nIf you want to take this \"denormalized\" approach check out [Anish Karandikar's](@anishkny) [integrify](https://www.npmjs.com/package/integrify) package which lets you declaratively set up relations between your collections. It automatically maintains Firestore triggers that synchronize the data between those collections.\n\n`use-firestore` takes a different approach. It encourages you do keep your data normalized, so there's a single source of truth. And then it helps you efficiently aggregate the queries needed to support your relations within a React app.\n\n|                                                   | use-firestore | react-firebase-hooks + integrify    |\n| ------------------------------------------------- | ------------- | ----------------------------------- |\n| React-based                                       | ✅            | ✅                                  |\n| Realtime updates                                  | ✅            | ✅                                  |\n| Fetch a sub-graph of documents with a single read | ❌            | ✅                                  |\n| Re-use queries application-wide                   | ✅            | ❌                                  |\n| Throws errors                                     | ✅            | ❌ requires manual error handling   |\n| Memory efficient derived state on top of queries  | ✅            | ❌ each hook returns unique objects |\n| Optimistic updates                                | ✅            | ✅ via the Firebase SDK?            |\n| Batch document reads to avoid N+1 problem         | ✅            | ❌                                  |\n\nOf course you could combine `use-firestore` with `integrify` to mix and match the benefits of the two approaches.\n\n## API Reference\n\n### `useQuery` hook\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\n\ntype User = {\n  id: string\n  name: string\n  email: string\n  teamId: string\n}\n\nexport function App() {\n  const [teamId] = useQueryParam(\"teamId\")\n\n  const users = useQuery(\n    query(\n      collection(getFirestore(app), \"users\"),\n      where(\"teamId\", \"==\", teamId)\n    )\n  )\n\n  if (!users) return null\n\n  return users.map((user) => <div>{user.name}</div>)\n}\n```\n\nIf you would like to create some sort of derived state from your Firestore data, which will be efficiently cached, you can use the `useGlobalMemo` hook.\n\nFor example, if you have a \"users\" collection and each user has N \"assignments\", you can wire this up the following way, such that you only query Firebase twice, and get an array of users each with an array of assignments:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nconst assignments = useQuery(\n  query(collection(getFirestore(app), \"assignments\"))\n)\n\nconst assignmentsByUserId = useGlobalMemo(\"assignmentsByUserId\", () => {\n  return groupBy(assignments, \"userId\")\n}, [assignments])\n\nconst userDocs = useQuery(\n  query(collection(getFirestore(app), \"users\"))\n)\n\nconst users = useGlobalMemo(\"users\", () => userDocs.map((user)) => ({\n  ...user,\n  assignments: assignmentsByUserId[user.id] ?? []\n}), [userDocs, assignmentsById])\n```\n\n### `useDoc` hook with optimistic updates\n\nThe `useDoc` hook returns both the document and an update function that immediately updates the document state while firing off a write to Firestore in the background:\n\n```tsx\nimport { useDoc } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { groupBy } from \"lodash\"\n\nfunction Repo({ repoId }) {\n  const [repo, updateRepo] = useDoc<Repo>(\n    doc(getFirestore(testApp), \"repos\", repoId)\n  )\n\n  if (!repo) return null\n\n  return (\n    <input\n      type=\"text\"\n      value={repo.name}\n      onChange={(event) => {\n        updateRepo({\n          name: event.target.value,\n        })\n      }}\n    />\n  )\n}\n```\n\n### `useDocs` hook\n\nThe `useDocs` hook obviously can be used to fetch multiple documents by ID in a single call....\n\n```tsx\nimport { useDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nconst tags = useDocs<Tag>(\n  collection(getFirestore(app), \"tags\"),\n  tagIds\n)\n\nif (!tags) return null\n\nreturn (\n  <>\n    {tags.map((tag) => (\n      <span key={tag.id} className={`tag-${tag.color}`}>\n        {tag.text}\n      </span>\n    ))}\n  </>\n)\n```\n\n... however it's most useful for efficient querying of 1:N relationships in a tree of React components:\n\n```\n[example needed]\n```\n\n### `deleteDocs` function\n\nBasic deletion:\n\n```ts\nimport { deleteDocs } from \"use-firestore\"\nimport { collection, getFirestore } from \"firebase/firestore\"\n\nawait deleteDocs(collection(getFirestore(app), \"tags\"), [\n  \"tag123\",\n  \"tag456\",\n  \"tag789\",\n])\n```\n\nAlso remove the deleted doc's `id` from the `tagIds` field on an associated collection:\n\n```ts\nimport { deleteDocs, andRemoveFromIds } from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andRemoveFromIds(\n    collection(getFirestore(app), \"repos\"),\n    \"tagIds\"\n  )\n)\n```\n\nDelete related docs with a 1:1 or 1:N relation:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [\"tag123\"],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\"\n  )\n)\n```\n\nThe above code will also delete any documents in the \"highlights\" collection which have the `tagId` field set to `\"tag123\"`, before deleting `/tags/tag123`.\n\n### Nested **delete**\n\nYou can also go multiple levels deep with your deletions. For example, if every \"highlight\" belongs to a \"tag\" and every \"document\" has many \"highlights\", when you delete a tag you want to:\n\n1. Delete all of the highlights associated with that tag\n2. Remove all of those highlights from any documents they are referenced in\n3. Finally, delete the highlights.\n\nThe code for that would look like:\n\n```ts\nimport {\n  deleteDocs,\n  andDeleteAssociatedDocs,\n  andRemoveFromIds,\n} from \"use-firestore\"\n\nawait deleteDocs(\n  collection(getFirestore(app), \"tags\"),\n  [tag.id],\n  andDeleteAssociatedDocs(\n    collection(getFirestore(app), \"highlights\"),\n    \"tagId\",\n    andRemoveFromIds(\n      collection(getFirestore(app), \"documents\"),\n      \"highlightIds\"\n    )\n  )\n)\n```\n\n## Warnings\n\nThe `deleteDocs` function will do all of the deletions and updates in a series of [batched writes](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes). However note that if there are more than 500 updates and/or writes to do, `deleteDocs` will do several batched writes. If any batch fails this can create inconsistencies in your data.\n\nIn addition, as part of its execution `deleteDocs` has to query the relations it will delete/update. If the underlying data is modified between when it does those queries and when the batches are committed, this can also introduce inconsistencies.\n\n## Why\n\nApplications can be built a lot more simply when individual components can request the data they need, without having to worry about triggering the N+1 problem.\n\nThis especially matters when working with Firestore because it's a non-relational database. That means joins must either be created manually at query time, or they must be updated manually every time either side of the relation changes.\n\nFor example, if I have a collection of stories, each of which has a number of tags, and I want to show a table that lists the stories along with their tags, I either have to:\n\n1. Copy the tag objects into the stories collection every time a tag changes color or is renamed, or\n2. Query the tags and the stories and then knit them together on the client\n\n**Option 1**—often called \"de-normalization\"—is a great option, but it means you need to maintain a lot of event triggers, and data can get out of sync.\n\n`use-firestore` is useful if you want to pursue **Option 2**—i.e. \"normalization\".\n\nInstead of copying the tags onto every story, you can efficiently maintain an index of tags to be looked up at the row level:\n\n```tsx\nimport { useQuery, useGlobalMemo } from \"use-firestore\"\nimport { query, getFirestore } from \"firebase/firestore\"\nimport { keyBy } from \"lodash\"\n\nfunction StoryTable() {\n  const stories = useQuery(\n    query(collection(getFirestore(app), \"stories\"))\n  )\n\n  if (!stories) return null\n\n  return (\n    <table>\n      {stories.map((story) => (\n        <StoryRow key={story.id} {...story} />\n      ))}\n    </table>\n  )\n}\n\nfunction StoryRow({ title, tagIds }) {\n  const tags = useQuery(\n    query(collection(getFirestore(app), \"tags\"))\n  )\n  const tagsById = useGlobalMemo(\n    \"tagsById\",\n    () => tags && keyBy(tags),\n    [tags]\n  )\n\n  if (!tagsById) return null\n\n  return (\n    <tr>\n      <td>\n        {title}\n        {tagIds.map((id) => {\n          const { name, color } = tagsById[id]\n          return (\n            <Badge key={id} color={color}>\n              {name}\n            </Badge>\n          )\n        })}\n      </td>\n    </tr>\n  )\n}\n```\n\nIn this scenario, we get a few nice performance benefits:\n\n1. The \"tags\" collection is only queried once, even if there are 50 rows in the table\n2. There will only be one `tags` array allocated in memory, and it will be used in all 50 rows\n3. The `keyBy` function will only be called once\n\nAdditionally, if we were to use that `tags` array as a prop to a memoized component, it would only trigger a re-render when the collection actually changes, regardless of how many times the parent component renders.\n\n## Todo\n\n- [x] Unsubscribe from query when no more listeners are left\n- [x] Add tests\n- [x] useDoc()\n- [x] useDocs()\n- [x] deleteDocs\n- [ ] For small collections, just query the entire thing instead of just getting a subset\n- [ ] Add post-processing/validation/type guard function to everything\n","readmeFilename":"README.md","gitHead":"2183a8c1ee2844b2998e2fac6da7c3e57f5be6d6","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>","_id":"use-firestore@0.18.0-beta.3","_nodeVersion":"18.12.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-7wj4RIac0ABZrBmVgdg9OJmtfKSfJtDedoHxj1TCPKbI+gcRCv+Hs1QXISZEYCvE3FS8Ok3H3AbnEFCszxuibg==","shasum":"fbfb88f56ac735820f07204fbf6095cfbd0756a5","tarball":"https://registry.npmjs.org/use-firestore/-/use-firestore-0.18.0-beta.3.tgz","fileCount":33,"unpackedSize":429054,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIDnrvdZFixsxnelWuDwHfOKQgtD7NSTJTj0B9PqaWHw8AiEA/fnjBvfpa6u3TNk+ApAxcdfv9l+dMI4p71Wsb3xZij8="}]},"_npmUser":{"name":"erikpukinskis","email":"erik@snowedin.net"},"directories":{},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/use-firestore_0.18.0-beta.3_1695761802202_0.9761414447514085"},"_hasShrinkwrap":false}},"time":{"created":"2023-05-28T09:27:03.872Z","0.2.0":"2023-05-28T09:27:04.076Z","modified":"2023-09-26T20:56:42.596Z","0.3.0":"2023-05-28T09:43:20.088Z","0.4.0":"2023-05-29T00:16:28.711Z","0.5.0":"2023-05-29T00:25:28.473Z","0.6.0":"2023-05-29T00:27:52.005Z","0.7.0-beta.0":"2023-06-09T17:33:40.864Z","0.7.0":"2023-06-09T18:00:21.582Z","0.8.0":"2023-06-11T22:52:35.225Z","0.9.0-beta.0":"2023-07-04T16:03:02.969Z","0.10.0-beta.0":"2023-07-20T19:19:23.320Z","0.10.0-beta.1":"2023-07-20T19:25:04.529Z","0.10.0-beta.2":"2023-07-20T19:38:05.638Z","0.10.0":"2023-07-20T20:11:26.733Z","0.11.0-beta.0":"2023-07-20T20:14:23.722Z","0.11.0-beta.1":"2023-07-21T03:53:11.446Z","0.11.0-beta.2":"2023-07-26T04:56:54.351Z","0.11.0-beta.3":"2023-07-26T04:57:57.663Z","0.11.0-beta.4":"2023-07-26T05:00:01.072Z","0.11.0":"2023-07-27T05:56:53.910Z","0.12.0-beta.0":"2023-07-28T15:53:11.108Z","0.12.0-beta.1":"2023-07-31T04:48:57.865Z","0.12.0":"2023-07-31T06:52:18.050Z","0.13.0":"2023-07-31T06:54:31.848Z","0.13.1-beta.0":"2023-08-09T18:53:03.548Z","0.14.0-beta.0":"2023-08-09T18:53:55.962Z","0.14.0-beta.1":"2023-08-09T19:17:00.747Z","0.14.0":"2023-08-09T20:12:09.609Z","0.15.0-beta.0":"2023-08-10T05:29:46.795Z","0.15.0":"2023-08-10T05:48:58.415Z","0.16.0-beta.0":"2023-08-25T16:17:54.916Z","0.17.0-beta.0":"2023-08-26T05:23:58.947Z","0.17.0-beta.1":"2023-08-26T15:20:44.824Z","0.17.0-beta.2":"2023-08-26T17:36:03.709Z","0.17.0-beta.3":"2023-08-26T18:21:46.801Z","0.17.0":"2023-08-26T19:25:40.208Z","0.18.0-beta.0":"2023-09-10T04:55:19.365Z","0.18.0-beta.1":"2023-09-19T05:52:55.020Z","0.18.0-beta.2":"2023-09-19T15:52:59.578Z","0.18.0-beta.3":"2023-09-26T20:56:42.449Z"},"maintainers":[{"name":"erikpukinskis","email":"erik@snowedin.net"}],"license":"MIT","readme":"","readmeFilename":"","description":"<p align=\"center\"> <img src=\"./icon.png\" width=\"128\" height=\"128\" alt=\"the use-firestore logo, a painting of a red can with a flame on the label\" /> </p>"}