Search code examples
javascriptreactjsreact-router-dom

When i access a react-router route directly via url, it redirects to the '/' route


Whenever I access the "/projects" route via url, it redirects to "/", but I can access it via a button for example. I am 100% sure it's something wrong with the AuthContext, because if I just put true on the condition arg on PrivateRoute component, it works, but if the condition come from the AuthContext, it's buggy.

This is my Router component:

// private route, if condition is true, it render the children component,
// else, redirect to 'redirectRoute'
function PrivateRoute({ condition, redirectRoute = '/login' }) {
  const auth = useAuth();

  if(auth.loading) return null;

  return (
    condition
      ? <Outlet></Outlet>
      : <Navigate to={redirectRoute}></Navigate>
  )
}

function Router() {
  // this useAuth() is basically a useContext(AuthContext)
  const auth = useAuth();

  return (
    <BrowserRouter>
      <Routes>
        <Route
          exact
          path="/"
          element={<PrivateRoute condition={auth.isLogged}></PrivateRoute>}
        >
          <Route exact path="/" element={<Home></Home>}></Route>
        </Route>
        <Route
          exact
          path="/projects"
          element={<PrivateRoute condition={auth.isLogged}></PrivateRoute>}
        >
          <Route
            exact
            path="/projects"
            element={<Projetos></Projetos>}
          ></Route>
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

And this is my AuthContext.js:

export const AuthContext = createContext({});

export function AuthProvider({children}) {
  const [isLogged, setIsLogged] = useState(false);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    // this function send the jwt token to backend to verify
    // if its valid, if so, it do a setIsLogged(true)
    async function checkToken() {
      const token = JSON.parse(localStorage.getItem('costs_token'));
      if (!token) return false;
      const headers = {
        'Content-Type': 'application/json',
        authorization: 'Bearer '+token
      }

      const response = await fetch('/auth/validatetoken', {
        method: 'GET',
        headers
      });

      if (response.status === 200) {
        setIsLogged(true);
        return true;
      }
      localStorage.removeItem('costs_token');
      return false;
    }

    setLoading(true);
    checkToken().then(() => setLoading(false));
  }, []);

  return (
    <AuthContext.Provider value={{isLogged, loading}}>
      {children}
    </AuthContext.Provider>
  );
}

I tried using a boolean value that do not come from AuthContext, it worked, but if the boolean come from the AuthContext, I can't access it directly via url, it redirects to "/".


Solution

  • Start the app with an initially true loading state value so the PrivateRoute "waits" for the initial auth check to complete. When loading is initially false the PrivateRoute component fails the if-loading condition and conditionally renders the protected content or the redirect based on the passed condition prop. Since auth.isLogged is also initially false the Navigate component is rendered.

    export function AuthProvider({ children }) {
      const [isLogged, setIsLogged] = useState(false);
      const [loading, setLoading] = useState(true); // <-- initially true
    
      useEffect(() => {
        // this function send the jwt token to backend to verify if its valid,
        // if so, it do a setIsLogged(true)
        async function checkToken() {
          const token = JSON.parse(localStorage.getItem('costs_token'));
          if (!token) return false;
          const headers = {
            'Content-Type': 'application/json',
            authorization: 'Bearer '+token
          }
    
          const response = await fetch('/auth/validatetoken', {
            method: 'GET',
            headers
          });
    
          if (response.status === 200) {
            setIsLogged(true);
            return true;
          }
          localStorage.removeItem('costs_token');
          return false;
        }
    
        setLoading(true);
        checkToken().then(() => setLoading(false));
      }, []);
    
      return (
        <AuthContext.Provider value={{isLogged, loading}}>
          {children}
        </AuthContext.Provider>
      );
    }
    
    function PrivateRoute({ condition, redirectRoute = '/login' }) {
      const auth = useAuth();
      
      // Wait until auth status confirmed/loaded
      if (auth.loading) return null;
    
      return condition
        ? <Outlet />
        : <Navigate to={redirectRoute} replace />;
    }