Each hook runs one flow end to end. It holds its own loading, error, and success state, and sets up the browser pieces and callbacks the flow needs. Every action returns a result you can branch on: { success: true, … } when it works, { success: false, error, code, cause } when it doesn't. Firebase's own response is on both paths.
Zero dependencies — firebase and react are peers.
- Each hook covers a whole flow, not one Firebase call.
useEmailLinkSignInreportsneedsEmailwhen it needs the address, so you render the input in your own UI.usePhoneSignIncreates and cleans up the reCAPTCHA verifier.useOAuthSignInfinishes a redirect sign-in when the page returns.useVerifyEmailreports how far the code got. - Server sessions built in.
onIdTokenhands you a fresh ID token after every sign-in — trade it for a session cookie in one line.onBeforeSignOutruns before Firebase clears the session, so a failed teardown leaves the user signed in. - Every action returns a result.
{ success: true, ... }or{ success: false, error, code, cause }, so a failed call is a value you read, not an exception you catch. - Firebase's own data, unmodified. Sign-ins return the raw
UserCredential; failures carry Firebase's error code and the original error. Message formatting is opt-in. - Automatic reauthentication. Pass
currentPasswordto a sensitive operation and the hook handles the recent-sign-in check; omit it andauth/requires-recent-loginreaches you. - Configure once or per call. Session callbacks, action-code settings, error wording, and the
onErrorobserver live on the provider; any hook can override them or opt out withnull. - Typed, tested, ESM + CJS. Built against Firebase 11/12 and React 18/19. The
"use client"banner tells React Server Component frameworks where the client boundary is.
npm install @timonwa/firebase-hooks firebase
# or: pnpm add @timonwa/firebase-hooks firebase · yarn add @timonwa/firebase-hooks firebaseRequires React 18 or 19 and Firebase 11 or 12 (both peer dependencies).
Create your Auth instance once with the Firebase SDK, then wrap your app:
import { AuthProvider } from "@timonwa/firebase-hooks/auth";
import { auth } from "@/lib/firebase"; // getAuth(initializeApp(config))
<AuthProvider auth={auth} onIdToken={(idToken) => createSession(idToken)}>
{children}
</AuthProvider>;Hooks below it need nothing passed:
import { useLogin } from "@timonwa/firebase-hooks/auth";
function LoginForm() {
const { login, loading, error } = useLogin();
async function onSubmit(email: string, password: string) {
const result = await login(email, password);
if (result.success) router.push("/dashboard");
}
}The provider is optional for everything except useAuth — pass your own instance instead (useLogin(auth)) and it wins over the provider's.
| Service | Import | Status |
|---|---|---|
| Core | @timonwa/firebase-hooks |
Available |
| Auth | @timonwa/firebase-hooks/auth |
Available |
| Firestore | @timonwa/firebase-hooks/firestore |
Coming soon |
| Storage | @timonwa/firebase-hooks/storage |
Coming soon |
| Cloud Functions | @timonwa/firebase-hooks/functions |
Coming soon |
The most-used services ship first; more (Realtime Database, Remote Config, Cloud Messaging, and others) may follow once these land.
Each service is its own import, so an app only carries the services it uses. The root holds what every service shares — formatFirebaseError, getFirebaseErrorCode, and the HookResult types.
import { formatFirebaseError, getFirebaseErrorCode } from "@timonwa/firebase-hooks";
import {
AuthProvider,
useLogin,
AUTH_ERROR_MESSAGES,
} from "@timonwa/firebase-hooks/auth";Every hook has its own page — signature, options, and a worked example — in the documentation.
One contract, so learning one hook is learning them all:
- Every hook needs an
Authinstance — taken fromAuthProvider, or passed in as the first argument to override it. No global, no hidden singleton. Passingnullmeans "not ready yet", never "use the provider's", so an action called beforeauthexists fails cleanly with{ success: false, error }. - Every action resolves to
HookResult—{ success: true, ...data }or{ success: false, error, code, cause }. The hook'serrorstate carries the same message for rendering, andloadingandsuccesstrack the action. See Error handling. onIdToken(idToken, user)runs after a successful sign-in, with a freshly minted token. Throw inside it to abort the flow; the error surfaces like any other.currentPasswordtriggers reauthentication inuseUpdatePassword,useUpdateEmail, anduseDeleteAccount.useReauthenticateexposes the same step for custom flows.- Provider options are defaults, hook options win.
onIdToken,onBeforeSignOut,actionCodeSettings,formatErrorMessage, and theonErrorobserver can all be set once onAuthProvider; a hook's own option overrides the provider, and an explicitnullopts that flow out entirely:
<AuthProvider
auth={auth}
onIdToken={(idToken) => createSession(idToken)}
onBeforeSignOut={() => clearSession()}
actionCodeSettings={{ url: `${origin}/auth/action`, handleCodeInApp: true }}
formatErrorMessage={(e) => formatFirebaseError(e, { messages: AUTH_ERROR_MESSAGES })}
>
{children}
</AuthProvider>;
const { login } = useLogin(); // auth and session minting inherited — nothing to wire// from "@timonwa/firebase-hooks" (core); AUTH_ERROR_MESSAGES from "…/auth"
// A failed action, in full:
{ success: false,
error: string, // processed message — raw by default
code: string | null, // Firebase's raw code: "auth/invalid-credential"
cause: unknown } // the complete untouched error
getFirebaseErrorCode(error: unknown): string | null
formatFirebaseError(error, options?: { messages?: Record<string, string>; fallback?: string }): string
AUTH_ERROR_MESSAGES: Record<string, string>With no configuration, error is the message Firebase produced. Formatting is opt-in via formatFirebaseError, which resolves in a fixed order. A match in messages wins. An unmapped Firebase error keeps Firebase's own words with the framing stripped: "Firebase: The email address is badly formatted. (auth/invalid-email)." becomes "The email address is badly formatted." Any other error passes through raw, so an error thrown from your own onIdToken or onBefore* callback arrives exactly as you threw it.
AUTH_ERROR_MESSAGES is the shipped auth/* catalogue with security-conscious copy (credential failures never reveal whether an account exists). Spread and override it for i18n or your own voice:
// Once, globally — every hook below the provider uses it; any hook's own option overrides it:
<AuthProvider
auth={auth}
formatErrorMessage={(e) => formatFirebaseError(e, { messages: AUTH_ERROR_MESSAGES })}
>
// i18n / custom copy per code:
formatFirebaseError(e, { messages: { ...AUTH_ERROR_MESSAGES, "auth/invalid-credential": "Email ou mot de passe incorrect." } })
// Or skip messages entirely and branch on raw codes yourself:
const result = await login(email, password);
if (!result.success && result.code === "auth/too-many-requests") startCooldown();If your formatter throws, error falls back to the raw message, so the failure still reaches you.
For logging and analytics, the provider's onError observer sees every failure from every hook. It receives the raw error and { action, code, message }, where action is a stable id such as "login" or "update-password". It is fire-and-forget: a throwing observer never affects the flow.
<AuthProvider
auth={auth}
onError={(error, { action, code }) => track("auth_error", { action, code })}
>
{children}
</AuthProvider>Future service catalogues ship with their own import (FIRESTORE_ERROR_MESSAGES with the Firestore hooks, STORAGE_ERROR_MESSAGES with Storage).
Every hook is a client hook. The built output carries a "use client" banner. In React Server Component frameworks you import from this package inside client components with no extra ceremony. Importing from a server component fails with the framework's own boundary error. Passing auth: null during initialisation is always safe: state hooks report signed-out/loading, and actions fail cleanly with { success: false, error, code, cause }.
Plain React hooks with no framework imports, so they run anywhere React runs. That covers Next.js (App or Pages Router), Remix / React Router, Gatsby, TanStack Start, Waku, plain Vite/CRA SPAs, and Astro's React islands.
React Native / Expo (with the Firebase web SDK): the email/password, anonymous, custom-token, logout, password, email, profile, reauthentication, and linking hooks work as-is. Web-only by nature: useOAuthSignIn (popup/redirect are browser concepts), usePhoneSignIn (reCAPTCHA needs the DOM), and useEmailLinkSignIn's email persistence (uses localStorage; a pluggable storage option is planned). The react-native-firebase native SDK is a different import surface and is not supported.
Not in Auth yet: multi-factor auth (TOTP/SMS enrolment and resolution) and multi-tenancy — planned for a later minor. The Admin SDK is server-side and out of scope.
Bug reports and PRs welcome — see CONTRIBUTING.md.