reactjsfirebasefirebase-authenticationreact-routerreact-context

redirection to the main page after logIn or registration doesn't working. firebase, private route, email authentication


I encountered an issue that I can't solve. In my handleLogIn function within the ContextProvider component, I am using the useNavigate hook to redirect the page to "/" if the user successfully logs in. However, after the user successfully logs in, they are not redirected to the main page; instead, they are redirected back to the login page. After the second form submission, the page redirection works correctly.

How can I redirect the user from the "/sign-in" or "/sign-up" page immediately after form submission?

here is my contextProviderComponent:

export const ContextAPI = createContext<null | TContextAPI>(null);

const ContextProvider = ({ children }: { children: React.ReactNode }) => {
  const navigate = useNavigate();
  const [currentUser, setCurrentUser] = useState<firebase.User | undefined | null>(undefined);
  const [currentUserId, setCurrentUserId] = useState<string | undefined | null>(undefined);
  const [loading, setLoading] = useState<boolean>(true);
  const { signUpValues, SignUpInputConstructor, handleRegister, signUpError } = useAuth();
  const { logInValues, SignInInputConstructor } = useLogIn();
  const { handleLogout, logOutError } = useLogOut();
  const { newGroupName, isShowGroupCreator, setIsShowGroupCreator, setNewGroupName } = useGroupMenu();

  const handleUserGroups = () => {
    if (currentUserId) {
      const groupRef = doc(db, `/user_groups/${currentUserId}/${newGroupName}`, uuid());
      if (newGroupName === "" || newGroupName.length > 20) {
        console.log("group name is incorrect");
      } else {
        setDoc(groupRef, { merge: true });
        console.log("db updated sucessfully");
        navigate("/");
      }
    } else {
      console.log("currentUserId is not set");
    }
  };

  const [logInError, setLogInError] = useState<string>("");
  const logIn = (email: string, password: string) => {
    return auth.signInWithEmailAndPassword(email, password);
  };

  const handleLogIn = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (!/^[a-zA-Z\s].*@.*$/.test(logInValues.email)) {
      return setLogInError("email is not correct");
    }
    if (logInValues.password === "") {
      return setLogInError("password field can not be empty");
    } else {
      try {
        setLogInError("");
        setLoading(true);
        logIn(logInValues.email, logInValues.password).then(() => {
          navigate("/");
        });
      } catch (errors) {
        setLogInError("Failed to log-in in account");
      }
      setLoading(false);
    }
  };

  const vals: TContextAPI = {
    setNewGroupName,
    isShowGroupCreator,
    setIsShowGroupCreator,

    newGroupName,
    currentUserId,
    handleUserGroups,
    handleLogout,
    logOutError,
    setLoading,
    currentUser,
    signUpValues,
    SignUpInputConstructor,
    handleRegister,
    loading,
    signUpError,
    logInValues,
    SignInInputConstructor,
    handleLogIn,
    logInError,
  };

  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged((user) => {
      setCurrentUser(user);
      setLoading(false);
      setCurrentUserId(user?.uid || null);
    });

    return unsubscribe;
  }, []);

  return <ContextAPI.Provider value={vals}> {!loading && children} </ContextAPI.Provider>;
};

and this is my LogIn component:

const LogIn = () => {
  const SignInInputConstructor = useContextSelector(ContextAPI, (v) => v?.SignInInputConstructor);
  const loading = useContextSelector(ContextAPI, (v) => v?.loading);
  const currentUser = useContextSelector(ContextAPI, (v) => v?.currentUser);
  const handleLogIn = useContextSelector(ContextAPI, (v) => v?.handleLogIn);
  const logInError = useContextSelector(ContextAPI, (v) => v?.logInError);
  return (
    <>
      <h1>Log In page</h1>
      <form
        onSubmit={(e) => {
          handleLogIn?.(e);
        }}
      >
        {currentUser?.email}
        {logInError && <Alert severity="error">{logInError}</Alert>}
        {SignInInputConstructor?.map((item) => (
          <>
            <Typography>{item.typography}</Typography>
            <TextField
              id={item.id}
              placeholder={item.placeholder}
              variant={item.variant}
              value={item.value}
              onChange={item.onChange}
            />
          </>
        ))}
        <Button variant="contained" type="submit" disabled={loading}>
          Log In
        </Button>
        <Typography>
          Need an account? <Link to={"/sign-up"}>Sign Up</Link>
        </Typography>
      </form>
    </>
  );
};

and this is my privateRoute component:

const PrivateRoute = () => {
  const currentUser = useContextSelector(ContextAPI, (v) => v?.currentUser);
  if (currentUser === undefined) return null;

  return currentUser ? <Outlet /> : <Navigate to="/sign-in" />;
};

export default PrivateRoute;

and the lastone is my main component with all routes:

const Main = () => {
  const router = createBrowserRouter(
    [
      {
        path: "/",
        element: <App />,
        children: [
          {
            path: "/sign-in",
            element: <LogIn />,
          },
          {
            path: "/sign-up",
            element: <Register />,
          },
          {
            path: "/",
            element: <PrivateRoute />,
            children: [
              {
                path: "/",
                element: <GroupMenu />,
                children: [
                  {
                    path: "/add-group",
                    element: <AddGroupModal />,
                  },
                ],
              },
              {
                path: "/group-content",
                element: <GroupContent />,
              },
              {
                path: "/dashboard",
                element: <Dashboard />,
              },
            ],
          },
        ],
      },
    ],
    { basename: "/thatswhy_items_counter/" }
  );

  return (
    <React.StrictMode>
      <ThemeProvider theme={theme}>
        <RouterProvider router={router} />
      </ThemeProvider>
    </React.StrictMode>
  );
};

If you need more details, here is the project repo on GitHub: repo-link Thanks to all of you for your help in advance!

I've tried to move my login functions from the custom login hook to the ContextProvider component. I've tried to manage the conditional loading state in the PrivateRoute component. I've also tried to update my currentUser state in the logIn function within the ContextProvider component. Unfortunately, it didn't help me. I think I have this issue because my logIn function needs more time to log in the user, and my handleLogIn function doesn't wait.

log-in issue gif


Solution

  • You've declared your router within the ReactTree, so when the component rerenders for any reason, router is redeclared and unmounts the old routing tree and mounts the new one, and this interrupts any active navigation actions.

    Move the router declaration out of the ReactTree.

    const router = createBrowserRouter([
      {
        path: "/",
        element: <App />,
        children: [
          {
            path: "/sign-in",
            element: <LogIn />,
          },
          {
            path: "/sign-up",
            element: <Register />,
          },
          {
            element: <PrivateRoute />,
            children: [
              {
                path: "/",
                element: <GroupMenu />,
                children: [
                  {
                    path: "/add-group",
                    element: <AddGroupModal />,
                  },
                ],
              },
              {
                path: "/group-content",
                element: <GroupContent />,
              },
              {
                path: "/dashboard",
                element: <Dashboard />,
              },
            ],
          },
        ],
      },
    ]);
    
    const Main = () => {
      return (
        <React.StrictMode>
          <ThemeProvider theme={theme}>
            <RouterProvider router={router} />
          </ThemeProvider>
        </React.StrictMode>
      );
    };
    

    Or memoize it so it can be provided as a stable reference.

    const Main = () => {
      const router = useMemo(createBrowserRouter([
        {
          path: "/",
          element: <App />,
          children: [
            {
              path: "/sign-in",
              element: <LogIn />,
            },
            {
              path: "/sign-up",
              element: <Register />,
            },
            {
              element: <PrivateRoute />,
              children: [
                {
                  path: "/",
                  element: <GroupMenu />,
                  children: [
                    {
                      path: "/add-group",
                      element: <AddGroupModal />,
                    },
                  ],
                },
                {
                  path: "/group-content",
                  element: <GroupContent />,
                },
                {
                  path: "/dashboard",
                  element: <Dashboard />,
                },
              ],
            },
          ],
        },
      ]), []);
      
      return (
        <React.StrictMode>
          <ThemeProvider theme={theme}>
            <RouterProvider router={router} />
          </ThemeProvider>
        </React.StrictMode>
      );
    };
    

    I recommend also converting your handleLogIn callback to an async function so you can await the logIn to resolve and manage the loading state better, i.e. to set loading false only after the log-in attempt has succeeded or failed.

    const handleLogIn = async (e: React.FormEvent<HTMLFormElement>) => {
      e.preventDefault();
      if (!/^[a-zA-Z\s].*@.*$/.test(logInValues.email)) {
        return setLogInError("email is not correct");
      }
      if (logInValues.password === "") {
        return setLogInError("password field can not be empty");
      }
    
      try {
        setLogInError("");
        setLoading(true);
    
        await logIn(logInValues.email, logInValues.password);
    
        navigate("/");
      } catch (errors) {
        setLogInError("Failed to log-in in account");
      } finally {
        setLoading(false);
      }
    };
    

    There still appears to be a bit of a synchronization issue between when logIn, or auth.signInWithEmailAndPassword, successfully authenticates a user and when Firebase can push an auth change to your onAuthStateChange listener which updates the currentUser state client-side.

    Here's a couple additional things you can try: