react router v6 won't switch to Home Page after Login Page: Warning: Can't perform a React state update on an unmounted component

Viewed 658

Note: using firebase v9

On my webapp, I'm trying to have it so that after a successful login we redirect the user to the main page, and if it's unsuccessful we stay on the login page.

I originally just tried using private router, but I believe the currentUser wasn't getting set by useAuth(); so it wasn't getting defined.

Then I tried using the async await method, but I got more errors.

Here is the error I get in the console log:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
    at RequireAuth (http://localhost:3000/static/js/main.chunk.js:262:82)

Here is my implementation in App.js:

import { useState } from "react";
import "./App.css";
import Signup from "./pages/Signup";
import Login from "./pages/Login";
import { BrowserRouter, Routes, Route, Link, Navigate, Outlet } from "react-router-dom";
import Home from "./pages/Home";
import { useAuth } from "./firebase";

function App() {
  const [mainSection, setMainSection] = useState("home");

  const d = new Date();
  const time = d.getHours();
  if (time >= 18 || time < 5) {
    document.documentElement.classList.add("dark");
  }

  // async function PrivateRoute({children}) {
    
  //   // setLoading(true);
  //   try{
  //     const currentUser = await useAuth();
  //     return currentUser ? children : <Navigate to="/login" />;
  //     // await login(emailRef.current.value, passwordRef.current.value);
  //   } catch {
  //     alert("Error! Authentication failed!!")
  //   }
  //   // setLoading(false);
  // }

  function RequireAuth() {
    const currentUser = useAuth();
    console.log('currentUser (private router): ', currentUser);
    if (!currentUser) { 
          return <Navigate to="/login"/>;
      }
    return <Outlet />
    // <Home mainSection={mainSection} setMainSection={setMainSection} />;
    // return auth ? children : <Navigate to="/signup" />;
  }
  

  return (
      <BrowserRouter>
      <Routes>
        <Route element={<RequireAuth/>}>
          <Route path="/" element={<Home mainSection={mainSection} setMainSection={setMainSection} />}/>
        </Route>
        <Route path="/login" element={<Login />} />
        <Route path="/signup" element={<Signup />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

And here is my implementation that holds useAuth:

// Import the functions you need from the SDKs you need
import { useEffect, useState } from "react";
import { initializeApp } from "firebase/app";
import { getAuth, createUserWithEmailAndPassword, signInWithEmailAndPassword, onAuthStateChanged } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
import { doc, setDoc } from "firebase/firestore";
import { v4 as uuidv4 } from "uuid";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "AIzaSyBN30k6RivLOuz7KToi_uD8V5s5cmyD9RM",
  authDomain: "auth-development-62c42.firebaseapp.com",
  projectId: "auth-development-62c42",
  storageBucket: "auth-development-62c42.appspot.com",
  messagingSenderId: "414005826367",
  appId: "1:414005826367:web:7b987851735426ebedf98a"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth();

export function signup(email, password) {
  return createUserWithEmailAndPassword(auth, email, password);
}

export function login(email, password) {
  return signInWithEmailAndPassword(auth, email, password);
}

// eventually write a logout function

export async function sendHabitToFirestore(uidPath, habitName) {
  const db = getFirestore();
  const habitId = uuidv4();
  const pathDocRef = doc(db, "users", uidPath, "user_habits", habitId);
  
  //initialize habitValues for the whole year
  const habitValues = []
  var d = new Date();
  var year = d.getFullYear();
  for (let day = 1; day <= 31; day++) {
    var dateString = year.toString() + '-01-' + day.toString(); 
    var value = { date: dateString, completed: false };
    habitValues.push(value);
  }

  await setDoc(pathDocRef, {
    name: habitName, 
    id: habitId,
    calendarData: habitValues,
  });
}

export function useAuth() {
  const [currentUser, setCurrentUser ] = useState();
  useEffect(() => {
    const unsub = onAuthStateChanged(auth, user => setCurrentUser(user));
    return unsub;
  }, [])

  return currentUser;
}

export default getFirestore();
1 Answers

Issue

So I've gathered that a user is on the "/login" path and authenticate and you redirect them to the home path "/". If I'm reading the code correctly it's at this point the RequireAuth wrapper mounts and checks the current user. The initial currentUser state is undefined, or in other words is a falsey value, and <Navigate to="/login"/> is rendered and the user is redirected back to the login page. This unmounts the RequireAuth but there's possibly the lingering enqueued state update.

Solution

Move the RequireAuth outside App. I don't think this is the direct cause of your issue, but it's simply an anti-pattern to declare React components within other React components. Each time App rerenders you actually are declaring a new RequireAuth component and any component state will be lost.

To resolve the routing issue, add an indeterminant "pending" state that is either authenticated nor unauthenticated, and don't commit to rendering the outlet or redirect until the wrapper component confirms the currentUser auth status. Using undefined works for this as it is neither a user object nor null with there is no user.

function RequireAuth() {
  const currentUser = useAuth();

  if (currentUser === undefined) {
    return null; // or loading indicator, etc...
  }

  return currentUser ? <Outlet /> : <Navigate to="/login"/>;
}
Related