Thore
Thore

Reputation: 1848

React How to wait till current user data is fetched after user has logged in

In my react app I have guest routes and protected routes. The protected routes are only accessible when the user is authenticated. Next to that, the main app routes are only accessible when the user has signed a contract and finished the onboarding. I'm keeping track of these steps with some extra properties assigned to the user.

My current flow is the following

  1. The user enters the app and the function fetchCurrentUser is triggered inside the AuthContext Provider. If the call to the database returns data the property isAuthenticated is set to true and the user data is set to the currentUser state. If the calls returns an (unauthorized) error isAuthenticated is set to false. Initially isAuthenticated is set to null so I can render a loader as long as isAuthenticated is null.

  2. Let's assume the user wasn't logged in. Since isAuthenticated was first null and now false the code isn't returning the <h1>Loading</h1> loader anymore but will return a route. Because / can't be accessed because isAuthenticated is false, the app will redirect the user to the /login page

  3. When the user fills in the credentials and submits the data a cookie is returned from the backend and set in the browser. Now I want to re-trigger the fetchCurrentUser function to collect the user information. * To do this I set isAuthenticated back to null and I navigate the user to the dashboard page /. Since isAuthenticated is null the spinner will show up instead but the route is already /.

  4. In the meantime fetchCurrentUser will do the api call with the cookie which will return the user data and set isAuthenticated to true.

Short note for step 3 and 4. I think there are better ways to handle this so please don't hesitate to share a better solution.

Maybe there is a way to call the fetchCurrentUser from the Login component and wait till the data is set and navigate the user afterwards? Because fetchCurrentUser is more than an api call and the submit function has to wait till the whole function is done I should work with a promise but inside a promise I can't use async/wait to wait for the api call.

  1. Now that isAuthenticated is true and the user data is known and stored in the AuthProvider the routes can be rendered again. Since / is a protected route the code will check if isAuthenticated is true and check to which route the user needs to be navigated. This part goes wrong Warning: Maximum update depth exceeded but I don't know what I'm missing.

Code to test things out with some fake calls the represent the flow via https://codesandbox.io/s/zealous-meitner-9t000f?file=/src/router/index.js

Login.js

const Login = () => {
    const { setIsAuthenticated } = useContext(AuthContext);
    const navigate = useNavigate();

    const resolver = useYupResolver(loginValidationSchema);
    const {
        handleSubmit,
        register,
        formState: { errors },
    } = useForm({ resolver });

    const submit = async (values) => {
        await authService.login(values);
        setIsAuthenticated(null);
        navigate('/');
    };

    return (
        <div className='w-full border border-grey-300 rounded-lg overflow-hidden shadow sm:mx-auto sm:w-full sm:max-w-md'>
            <div className='p-4'>
                <form onSubmit={handleSubmit(submit)} className='space-y-6'>
                    <Input type='text' label='email' name='email' register={register} error={errors.email} />
                    <Input type='password' label='password' name='password' register={register} error={errors.password} />
                    <Button type='submit' label='login' color='tenant-primary' />
                </form>
            </div>
        </div>
    );
};

AuthContext

const AuthProvider = ({ children }) => {
    const [isAuthenticated, setIsAuthenticated] = useState(null);
    const [currentUser, setCurrentUser] = useState(null);

    useEffect(() => {
        if (isAuthenticated !== false) {
            getCurrentUser();
        }
    }, [isAuthenticated]);

    const getCurrentUser = async () => {
        try {
            const { data } = await authService.me();
            setIsAuthenticated(true);
            setCurrentUser(data);
        } catch (error) {
            setIsAuthenticated(false);
            setCurrentUser(null);
        }
    };

    return <AuthContext.Provider value={{ isAuthenticated, setIsAuthenticated, getCurrentUser, currentUser }}>{children}</AuthContext.Provider>;
};

router.js

const Router = () => {
    const authContext = useContext(AuthContext);

    const GuestRoute = () => {
        return !authContext.isAuthenticated ? <Outlet /> : <Navigate to='/' replace />;
    };

    const ProtectedRoutes = () => {
        if (!authContext.isAuthenticated) return <Navigate to='/login' replace />;
        else if (!authContext.currentUser?.settings?.is_contract_signed) return <Navigate to='/contract/sign' replace />;
        else if (!authContext.currentUser?.settings?.is_onboarding_finished) return <Navigate to='/onboarding' replace />;
        else return <Outlet />;
    };

    if (authContext.isAuthenticated === null) {
        return <h1>Loading ...</h1>;
    }

    return (
        <Routes>
            <Route element={<GuestRoute />}>
                <Route path='/login' element={<Login />} />
            </Route>

            <Route element={<ProtectedRoutes />}>
                <Route path='/' element={<Navigate to='/onboarding' replace />} />
                <Route path='/contract/sign' element={<SignContract />} />
                <Route path='/onboarding' element={<Onboarding />} />
                <Route path='/profile' element={<Profile />} />
            </Route>

            <Route path='404' element={<NotFound />} />
            <Route path='*' element={<Navigate to='/404' replace />} />
        </Routes>
    );
};

Upvotes: 3

Views: 5673

Answers (2)

Drew Reese
Drew Reese

Reputation: 203208

Issue

The is caused by the ProtectedRoutes unconditionally redirecting to authenticated routes.

const ProtectedRoutes = () => {
  if (!authContext.isAuthenticated)
    return <Navigate to="/login" replace />;         // <-- here
  else if (!authContext.currentUser?.settings?.is_contract_signed)
    return <Navigate to="/contract/sign" replace />; // <-- here
  else if (!authContext.currentUser?.settings?.is_onboarding_finished)
    return <Navigate to="/onboarding" replace />;    // <-- here
  else return <Outlet />;
};

This rerenders the route which rerenders the ProtectedRoutes component which triggers another redirect, repeat ad nauseam.

Solution

The ProtectedRoutes component should only concern itself with protecting access to a route or redirecting to another route to authenticate. The redirecting to specific protected routes based on user properties should occur in the login logic.

Additionally I highly recommend moving the GuestRoute and ProtectedRoutes component declarations out of the Router component. When these components are redeclared each render cycle it will necessarily unmount and remount their entire sub-ReactTree.

router/index.js

import React, { useContext } from "react";
import { Routes, Route, Outlet, Navigate } from "react-router-dom";

import Login from "../pages/Login";
import SignContract from "../pages/SignContract";
import Onboarding from "../pages/Onboarding";
import Dashboard from "../pages/Dashboard";

import { AuthContext } from "../context/AuthContext";

const GuestRoute = () => {
  const authContext = useContext(AuthContext);

  return !authContext.isAuthenticated ? (
    <Outlet />
  ) : (
    <Navigate to="/" replace />
  );
};

const ProtectedRoutes = () => {
  const authContext = useContext(AuthContext);

  return authContext.isAuthenticated ? (
    <Outlet />
  ) : (
    <Navigate to="/login" replace />
  );
};

const Router = () => {
  const authContext = useContext(AuthContext);

  if (authContext.isAuthenticated === null) {
    return <h1>Loading...</h1>;
  }

  return (
    <Routes>
      <Route element={<GuestRoute />}>
        <Route path="/login" element={<Login />} />
      </Route>

      <Route element={<ProtectedRoutes />}>
        <Route path="/" element={<Navigate to="/dashboard" replace />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/contract/sign" element={<SignContract />} />
        <Route path="/onboarding" element={<Onboarding />} />
      </Route>
    </Routes>
  );
};

export default Router;

pages/Login.js

The handleSubmit handler should receive the returned user object from the auth service and check the "roles" here to redirect to the appropriate authenticated route.

const handleSubmit = async (e) => {
  e.preventDefault();

  const data = await authService.login(formData);

  if (data) {
    const user = await authService.me();
    if (user) { 
      authContext.setIsAuthenticated(true);
      if (user?.settings?.is_contract_signed) {
        navigate("/contract/sign", {replace: true});
      } else if (user?.settings?.is_onboarding_finished) {
        navigate("/onboarding", {replace: true} );
      } else {
        navigate("/", { replace: true});
      }
    }
  }
};

The login logic might also want to implement this in a try/catch to handle any Promise rejections and other thrown errors, and also handle the case where the authentication fails. For example, should there be an error message, or redirect to another special page, etc. Basically this code should handle both the happy and unhappy paths.

Edit react-how-to-wait-till-current-user-data-is-fetched-after-user-has-logged-in

Upvotes: 1

Hacaga Hesenli
Hacaga Hesenli

Reputation: 19

It shows "Warning: Maximum update depth exceeded" because when you call getCurrentUser() method if isAuthenticated not false the method triggered 2 states and React sees that there are two states and React updated two states in the same time (automatic batching) and when isAuthenticated get new value useEffect() works again it sees that isAuthenticated true ant it is again changes states it happens again again and to infinity for that it show that error

Upvotes: 0

Related Questions