signInWithRedirect in React (Vite): a minimal example that works

Updated Oct 27, 2026 · Pewterkit Guides

In React, call getRedirectResult once at module level (not inside useEffect), read the signed-in user only from onAuthStateChanged, and make router guards wait until auth has settled. That avoids the two classic React bugs: a lost redirect result under StrictMode, and a guard that bounces the user back to /login before Firebase finishes the redirect.

Why redirect sign-in misbehaves in React

Three facts from the Firebase docs and SDK source drive the design:

  1. The redirect result is handed out once. In the current SDK source, the outcome goes to the first getRedirectResult call of the page load; later calls get null.
  2. StrictMode runs effects twice in development. React's docs: "Your components will re-run Effects an extra time to find bugs caused by missing Effect cleanup." A getRedirectResult call inside useEffect therefore runs twice. If the first run's result is discarded by a cleanup flag, the second run gets null and the result is gone.
  3. Auth state is not known at first render. auth.currentUser "might also be null because the auth object has not finished initializing," and with redirects "the onAuthStateChanged observer waits until getRedirectResult resolves before triggering." A guard that redirects as soon as user is null fires too early.

The fix: one shared promise for the redirect result, one listener with cleanup, and a loading flag that every guard respects.

Before you start: on localhost and on any domain that is not your authDomain, redirect sign-in also needs the storage-partitioning setup. For dev, see signInWithRedirect on localhost; for production, see signInWithRedirect not working.

Step 1: install

npx create-vite@latest   # choose React, then cd into the new folder
npm i firebase react-router

(React Router's current docs import everything from react-router; checked Oct 2026, latest 8.5.0.)

Step 2: firebase.js — call getRedirectResult exactly once

// src/firebase.js
import { initializeApp } from "firebase/app";
import { getAuth, GoogleAuthProvider, getRedirectResult } from "firebase/auth";

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
};

export const app = initializeApp(firebaseConfig);
export const auth = getAuth(app); // includes the browser redirect resolver
export const googleProvider = new GoogleAuthProvider();

// Runs once per page load, outside React, so StrictMode cannot call it twice.
// Errors are captured so an unused promise never becomes an unhandled rejection.
export const redirectOutcome = getRedirectResult(auth).then(
  (result) => ({ result, error: null }),
  (error) => ({ result: null, error })
);
# .env.local
VITE_FIREBASE_API_KEY=...
VITE_FIREBASE_AUTH_DOMAIN=...
VITE_FIREBASE_PROJECT_ID=...
VITE_FIREBASE_APP_ID=...

Vite only exposes variables prefixed with VITE_ to client code.

Step 3: AuthProvider.jsx — one listener, StrictMode-safe

// src/AuthProvider.jsx
import { createContext, useContext, useEffect, useState } from "react";
import { onAuthStateChanged } from "firebase/auth";
import { auth, redirectOutcome } from "./firebase";

const AuthContext = createContext({ user: null, loading: true, error: null });

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // StrictMode subscribes, unsubscribes, subscribes again in dev.
    // Returning the unsubscribe function leaves exactly one listener.
    const unsubscribe = onAuthStateChanged(auth, (u) => {
      setUser(u);
      setLoading(false);
    });
    return unsubscribe;
  }, []);

  useEffect(() => {
    let active = true;
    // Same promise on both StrictMode runs, so the outcome is never lost.
    redirectOutcome.then(({ error }) => {
      if (active && error) setError(error);
    });
    return () => {
      active = false;
    };
  }, []);

  return (
    <AuthContext.Provider value={{ user, loading, error }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);

You do not need result.user from the redirect result to know who signed in; the listener delivers that user. Use result only when you need the Google access token (GoogleAuthProvider.credentialFromResult(result)) or first-sign-in details.

Step 4: Login.jsx — start the redirect, then route on return

// src/Login.jsx
import { useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router";
import { signInWithRedirect } from "firebase/auth";
import { auth, googleProvider } from "./firebase";
import { useAuth } from "./AuthProvider";

const RETURN_KEY = "returnTo";

export function Login() {
  const { user, loading, error } = useAuth();
  const [startError, setStartError] = useState(null);
  const navigate = useNavigate();
  const location = useLocation();

  // Idempotent: reading (not removing) the key means StrictMode's second
  // run navigates to the same place instead of falling back to "/".
  useEffect(() => {
    if (!loading && user) {
      navigate(sessionStorage.getItem(RETURN_KEY) || "/", { replace: true });
    }
  }, [loading, user, navigate]);

  function handleClick() {
    // Keep the return path in sessionStorage so it is still there after the full-page round trip.
    sessionStorage.setItem(RETURN_KEY, location.state?.from ?? "/");
    signInWithRedirect(auth, googleProvider).catch(setStartError);
  }

  if (loading) return <p>Checking sign-in…</p>;

  const shown = startError || error;
  return (
    <main>
      {shown && <p role="alert">Sign-in failed: {shown.code}</p>}
      <button onClick={handleClick}>Continue with Google</button>
    </main>
  );
}

After Google, Firebase's handler sends the user back to the URL where sign-in started (here /login), so this page is also where the user lands after sign-in.

Step 5: RequireAuth.jsx — a guard that waits

// src/RequireAuth.jsx
import { useEffect } from "react";
import { useLocation, useNavigate } from "react-router";
import { useAuth } from "./AuthProvider";

export function RequireAuth({ children }) {
  const { user, loading } = useAuth();
  const navigate = useNavigate();
  const location = useLocation();

  useEffect(() => {
    if (!loading && !user) {
      navigate("/login", { replace: true, state: { from: location.pathname } });
    }
  }, [loading, user, navigate, location.pathname]);

  if (loading || !user) return <p>Loading…</p>; // never decide while loading
  return children;
}

Step 6: main.jsx — wire it up (StrictMode stays on)

// src/main.jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter, Routes, Route } from "react-router";
import { signOut } from "firebase/auth";
import { auth } from "./firebase";
import { AuthProvider, useAuth } from "./AuthProvider";
import { Login } from "./Login";
import { RequireAuth } from "./RequireAuth";

function Home() {
  const { user } = useAuth();
  return (
    <main>
      <p>Signed in as {user.email}</p>
      <button onClick={() => signOut(auth)}>Sign out</button>
    </main>
  );
}

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <AuthProvider>
      <BrowserRouter>
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route path="/" element={<RequireAuth><Home /></RequireAuth>} />
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  </StrictMode>
);

After sign-out, the listener sets user to null and RequireAuth sends the user to /login.

This example follows the current React, React Router and Firebase APIs but depends on your project's redirect setup. Test this on a preview deployment before relying on it.

How to confirm it worked

  1. Run npm run dev with StrictMode on. Open /: you land on /login without a flash of the protected page.
  2. Click Continue with Google. The browser goes to https://<your authDomain>/__/auth/handler, then to Google.
  3. After Google, you return to /login, see "Checking sign-in…", then land on / signed in. No "Sign-in failed" message.
  4. Add console.log inside the redirectOutcome.then callback: it logs a result with operationType: "signIn" right after sign-in, and null on a plain reload. Both are correct.
  5. Run npm run build && npm run preview and repeat: production has no StrictMode double run, and behavior should match.
  • Sign-in works in dev but returns signed out in production: the storage-partitioning setup is missing for your production domain.
  • You only want a quick working Google button without redirect: the general setup guide is Vite + React Firebase Google sign-in, and the trade-offs are in popup or redirect.

FAQ

Why does getRedirectResult return null in my React app?

A common cause in React is a second call. React StrictMode re-runs effects in development, and the current SDK returns the redirect outcome only to the first call in a page load. Call it once at module level and share the promise.

Should I turn off StrictMode to fix it?

No. StrictMode's extra effect run is development-only and points at code that is not idempotent. Moving getRedirectResult out of the effect fixes the cause, and the app then behaves the same in production.

Where should getRedirectResult go in a React app?

In the module that creates auth, called once when the page loads. Components read the shared promise, and they read the user from onAuthStateChanged.

Why does my router send me back to /login after Google sign-in?

The guard ran before Firebase finished processing the redirect. Keep a loading flag that turns false only in the first onAuthStateChanged callback, and do not redirect while it is true.

Sources

  1. Authenticate Using Google with JavaScript
  2. Manage Users in Firebase
  3. firebase/auth module reference (getRedirectResult, signInWithRedirect, getAuth)
  4. firebase-js-sdk source: core/strategies/redirect.ts
  5. firebase-js-sdk source: platform_browser/popup_redirect.ts
  6. StrictMode (React docs)
  7. useEffect (React docs)
  8. React Router: Installation (declarative mode)
  9. React Router: useNavigate
  10. Vite env variables and modes
  11. Window: sessionStorage property (MDN)

How this page was made: drafted with AI from the sources listed, reviewed by Pewterkit. Docs checked Oct 27, 2026. Spotted something out of date? Tell us.

Related guides