skip to Main Content

I’m building a React application using Redux for state management. My app checks if the user is already logged in by fetching authentication data from localStorage and dispatching it to Redux. However, I notice a brief flash of the login form on page refresh, even when the user is authenticated. Here’s my code:

function App() {
    const user = useSelector(({ user }) => user);
    const dispatch = useDispatch();
    const [isLoading, setIsLoading] = useState(true);

    async function getUserFromLocalStorage() {
        setIsLoading(true);
        if (!user) {
            const userAuthString = window.localStorage.getItem("userAuth");
            if (userAuthString !== null) {
                const userAuth = JSON.parse(userAuthString); 
                dispatch(loginUserAuth(userAuth));
            }
        }
        setIsLoading(false);
    }

    useEffect(() => {
        getUserFromLocalStorage();
    }, [dispatch]);

    if (isLoading) {
        return (
            <div className='bg-background w-screen h-screen flex items-center justify-center'>
                <SpinnerCircular />
            </div> 
        );
    }

    if (user === null) {
        return (
            <div className='bg-background w-svw h-svh'>
                <Notification />
                <LoginForm />
            </div> 
        );
    }

    return (
        <div className='bg-background w-screen h-screen flex flex-col'>
            <Notification />
            <Routes>
                <Route path="/" element={<Navbar />}>
                    <Route index element={<Home />} />
                    <Route path="/friends" element={<Friends />} />
                    <Route path="/exercises" element={<Exercises />} />
                    <Route path="/food" element={<Food />} />
                    <Route path="/settings" element={<Settings />} />
                </Route>
            </Routes>
        </div>
    );
}

Problem

I suspect the problem lies in how I am using useEffect. Is there a better way to manage this side-effect, particularly for state updates that rely on asynchronous operations? How can I prevent the flicker of the login form while the useEffect is fetching user data and updating the Redux state? Any suggestions would be greatly appreciated.

2

Answers


  1. Yeah, I can see where there’s possibly a gap between the isLoading state being toggled false and triggering App to rerender before user has been updated in the Redux store and another rerender triggered with the current value.

    My recommendation here would be to implement actual protected routes instead of conditionally rendering your login form or app routes. Update the Redux state to start with an initially undefined state.user value that App will dispatch an action to update to either null or a defined user object that was stored in localStorage.

    Example:

    import { Navigate, Outlet, useLocation } from 'react-router-dom';
    
    const ProtectedRoute = () => {
      const location = useLocation();
      const user = useSelector(state => state.user);
    
      // While user is not loaded yet, e.g  still undefined,
      // return loading UI
      if (user === undefined) {
        return (
          <div className='bg-background w-screen h-screen flex items-center justify-center'>
            <SpinnerCircular />
          </div>
        );
      }
    
      // Is user is null redirect to login page, otherwise render Outlet
      // for nested routes to render element content
      return user
        ? <Outlet />
        // pass current location to login page so user can be redirected
        // back after they authenticate
        : <Navigate to="/login" replace state={{ from : location }} />;
    };
    
    function App() {
      const dispatch = useDispatch();
      const [isLoading, setIsLoading] = useState(true);
    
      async function getUserFromLocalStorage() {
        setIsLoading(true);
    
        dispatch(loginUserAuth(
          // Will be null if nothing stored under "unserAuth" in localStorage
          JSON.parse(window.localStorage.getItem("userAuth"))
        ));
    
        setIsLoading(false);
      }
    
      useEffect(() => {
        getUserFromLocalStorage();
      }, [dispatch]);
    
      // While loading user data return loading UI
      if (isLoading) {
        return (
          <div className='bg-background w-screen h-screen flex items-center justify-center'>
            <SpinnerCircular />
          </div> 
        );
      }
    
      return (
        <div className='bg-background w-screen h-screen flex flex-col'>
          <Notification />
          <Routes>
            <Route path="/" element={<Navbar />}>
              <Route element={<ProtectedRoute />}>
                <Route index element={<Home />} />
                <Route path="friends" element={<Friends />} />
                <Route path="exercises" element={<Exercises />} />
                <Route path="food" element={<Food />} />
                <Route path="settings" element={<Settings />} />
              </Route>
              <Route path="login" element={<LoginForm />} />
            </Route>
          </Routes>
        </div>
      );
    }
    
    Login or Signup to reply.
  2. Try passing an empty array to your useEffect. The dispatch function that you passed to the useEffect will be a new object in each re-render, which will cause repeated calls to the useEffect.

    Note: I contribute to the state-machine-react library, which I find more readable and maintainable, for simple to medium-complexity projects.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search