Bug reports, fixes, and new hooks are all welcome. For anything bigger than a small fix, open an issue first so we can agree on the shape before you write it.
You need Node 20+ and pnpm (run corepack enable to get the version the repo pins). Then install and run the full quality gate:
pnpm install
pnpm verify # typecheck, lint, test, build, publint, attw — the same gate CI runsThis is a pnpm workspace. The published package lives in packages/firebase-hooks/, and every root script delegates to it, so pnpm test and pnpm build work from the repo root. Paths below are relative to packages/firebase-hooks/.
- Firebase client-SDK flows any React app can use. The Services table in the README tracks what's live and what's next; a new service starts as its own folder and subpath entry. Nothing tied to a product or a framework (no
next/*imports).firebaseandreactstay peer dependencies; the Admin SDK is server-side and out of scope. - Server integration through optional callbacks only (
onIdToken,onBeforeSignOut, …) — never a required action protocol or a result envelope a consumer must implement. Throwing inside a callback aborts the flow. - Nothing hard-coded. Storage keys,
actionCodeSettings, and error wording are parameters with sensible defaults. - Client hooks only. This package is client-side by design; everything ships behind a
"use client"banner.
- One folder per service (
src/core/,src/auth/, latersrc/firestore/,src/storage/), each with its ownindex.tsentry barrel. One file per hook, kebab-cased after it —src/auth/use-login.ts. Start the file with a"use client"directive and a JSDoc block; the JSDoc is what editors show, so keep it agreeing with the README. Internals a service shares live in that folder's_shared.tsand never reach the barrel. - Follow the shared contract.
auth: Auth | nullfirst argument; actions resolve toHookResultand never throw (useAuthTaskgives you the skeleton); sensitive operations reauthenticate first. - Options go in an exported
Use<Name>Optionsinterface extendingHookErrorOptions, with a TSDoc line on every field it declares itself (and@defaultValuewhere there is one). The docs site generates its options table from that interface, so an undocumented field ships an empty cell. Re-export it from the barrel too, so an app can type a wrapper around the hook without restating the shape. - Export it explicitly from its service's entry barrel (
src/auth/index.tsfor auth), one line per file, alphabetical. The root entry (src/core/index.ts) carries only the service-agnostic core — nothing service-specific is ever added to it; a new service gets a new folder + subpath entry. - Document it in the same change — add a page under
apps/docs/content/docs/auth/and list it in that folder'smeta.json. Follow the shape of the existing pages: prose intro, example,## Returns, then<AutoTypeTable>for the options. The table generates from the option interface's TSDoc, so document each field there rather than hand-writing a table. - Add it to the playground in the same change — a section component under
apps/playground/components/<service>/, rendered from that service's page, and its name in the right group inapps/playground/lib/hooks-map.tsso it appears in the sidebar. See below. - A hook ships with its test file beside it (
src/auth/use-login.test.tsx), importing through that service's barrel. Shared fakes and builders live in that folder's_test-helpersfile (never exported from the barrel); cross-cutting behaviour — the error model, formatter precedence, global config inheritance, theonErrorobserver — lives insrc/auth/auth-provider.test.tsx; thefirebase/authmock lives in the package's__mocks__/directory, activated per file with a barevi.mock("firebase/auth"). What's under test is the hook's orchestration (ordering, callbacks, error paths), not Firebase. Cover the edges: the nullauth, the throwing callback, the signed-out user.
Unit tests mock firebase/auth, so they prove the orchestration and nothing about Firebase itself. apps/playground/ is where you check the other half: a local Next app that runs every hook against a live project, one page per group, with the hook's loading, error, and resolved value shown beside each form.
It is never deployed — it exists so you can exercise a change before opening the PR. Bring your own Firebase project; apps/playground/README.md covers the .env.local values and the console settings each flow needs.
pnpm build # the playground resolves the library through its exports map
pnpm --filter playground devBecause it imports @timonwa/firebase-hooks the way a consumer does, a broken export or a missing subpath entry fails here rather than after publish — which is why CI builds it on every PR. Rebuild the library after changing it; the playground reads dist, not src.
Each hook is its own section component, one file per hook, rendered from its service's page — so a service page is a list of imports and a new hook is a new file rather than an edit to a growing one. The component renders a <HookSection>: the hook's name, a sentence on why it exists, the snippet, the form, and the hook's own loading/error/result passed straight through. Anything about running it locally goes in the playground's own README, not this file.
-
Fork the repo and create a branch from
main. -
Make your change and run
pnpm verify— a green run locally means a green PR. -
Add a changeset for anything that affects the published package:
pnpm changeset
Pick the bump — patch for a fix, minor for a new hook or option, major for a breaking change — and describe it in a sentence a consumer would understand.
-
Open a pull request against
maindescribing what changed and why.
Your changeset joins a "Version Packages" PR that changesets keeps open on main; when that PR merges, every pending change publishes to npm in one release. So your change ships with the next version merge rather than the moment your PR lands — and none of it needs credentials or npm access from you.