Home > Software design >  React protected route not redirecting to children when authorisation finished
React protected route not redirecting to children when authorisation finished

Time:02-07

I'm trying to put in a layer of authentication in my protected routes and I've been doing this using the following:

function ProtectedRoute ({ children }) {
    const { user } = useContext(AuthContext);
    
    return user.isVerifying ? (
        <p>Is loading...</p>
    ) : user.verified ? children : <Navigate to="/login" />;
}

export default ProtectedRoute;

My user context is initialised with the following information:

{
    "user": null,
    "verified": false,
    "isVerifying": false
}

At the moment, the user authorisation state is checked by a function (authUser), inside of my AuthContext.Provider, that at the moment runs on every render of my components (shown below). This is an asynchronous function that makes an API call to check to see if the user JWT token stored in localStorage is valid.

import React, { createContext, useEffect, useState } from 'react';
import axios from 'axios';

const AuthContext = createContext();

function AuthProvider({children}) {
    const [user, setUser] = useState({
        "user": null,
        "verified": false,
        "isVerifying": true
    });

    async function authUser() {
        const userLocal = localStorage.getItem('user');

        if (userLocal) {
            const userLocalJSON = JSON.parse(userLocal);
            const verifyResponse = await verifyToken(userLocalJSON);

            if (verifyResponse) {
                setUser({"user": userLocalJSON, "verified": true, "isVerifying": false});
            } else {
                setUser({"user": userLocalJSON, "verified": false, "isVerifying": false});
            }
        }
    }

    async function verifyToken(userJSON) {
        try {
            setUser({"user": null, "verified": false, "isVerifying": true});

            // Make API call

            if (response.status === 200) {
                return true;
            } else {
                return false;
            }
        } catch (e) {
            return false;
        }
    }

    useEffect(() => {
        authUser();
    }, [])

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

export default AuthContext;
export { AuthProvider };

Upon validating the token, the user context is then updated to verified = true with the information about the user. During this process, the user context isVerifying = true (an equivalent to isLoading) and gets set back to false once the API call is complete.

Now there are a few problems with this:

  1. On every re-render of the DOM, the context information is set back to its defaults. Is there a way around this so that useEffect() only fires upon localStorage changes? I've tried implenting the following solution, which adds a listener to localStorage and then triggers the useEffect when that changes. I could not get this to fire upon localStorage changes. Maybe I did something wrong (see below)?

     useEffect(() => {
       window.addEventListener('storage', authUser)
    
       return () => {
         window.removeEventListener('storage', authUser)
       }
     }, [])
    
  2. Because the initial isVerifying = false and the switch to true doesn't occur before the DOM is rendered, the protected route navigates to the login page. Even after the user context is finally set, it remains on the login page. The loading page is never displayed and instead there is an instant redirection to the login page. Unfortunately, I can't set isVerifying = true initially as non-logged in users get stuck on the loading page.

Any suggestions would be really helpful.

CodePudding user response:

To solve question 2:

Your ternary isn't quite doing what you want. It's kind of like saying "if user.isVerifying or !user.verified, navigate". That's not what we want.

Embedded ternaries can be tricky and I usually try to avoid them if I can. Since this component is so small, you don't need to do this with ternaries at all actually. I think this should work:

Set isVerifying to start out true. Then:

function ProtectedRoute ({ children }) {
    const { user } = useContext(AuthContext);

    if (user.isVerifying) {
        return <p>Is loading...</p>
    }

    if (user.verified === false) {
        return <Navigate to="/login" />
    }

    return children

}

export default ProtectedRoute;

EDIT: You also need to update the verification state in authUser. Noticed how you are only updating the loading state if userLocal is truthy. Best bet is to start out loading, and always call setUser at the end. This isn't the only way to do it but I think it should work:

    async function authUser() {
        setUser(currentState => {...currentState, isVerifying: true})
        const userLocal = localStorage.getItem('user');
        
        if (!userLocal) {
            setUser({
                user: null,
                verified: false,
                isVerifying: false,
            })
        }

        const userLocalJSON = JSON.parse(userLocal);
        const verifyResponse = await verifyToken(userLocalJSON);
        setUser({
            user: userLocalJSON,
            verified: verifyResponse,
            isVerifying: false,
        })
    }
  •  Tags:  
  • Related