1

im making a react app and im trying to make protected routes with the data i receive from the server (if the user is authenticated or not or if he is admin). the problem is that while im waiting for the data to fetch from the server i get a flicker to my 404 fallout route.

My target: render protected routes without flicker to the fallout 404 \ page not found route

The problem: because of the time the client takes to send and receive the data back from the server , it creates a small time that the client does not know that the user is authenticated and sends him to the fallout page and only after finishing the data fetching it redirects him back to the actual page.

my app.tsx -

import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { bindActionCreators } from 'redux';
import { useDispatch, useSelector } from 'react-redux';
import axios from 'axios';

import { ReducersState } from './state/reducers';
import * as actionCreators from './state/reducers/actionCreator';

import { User } from './utils/types';

import Home from './components/pages/Home/Home';
import Login from './components/pages/Login/Login';
import Register from './components/pages/Register/Register';
import Footer from './components/ui/Footer/Footer';
import NavBar from './components/ui/NavBar/NavBar';
import PageNotFound from './components/pages/404/PageNotFound';
import Contact from './components/pages/Contact/Contact';
import ProductsPage from './components/pages/ProductsPage/ProductsPage';
import ProductPage from './components/pages/ProductPage/ProductPage';
import Profile from './components/pages/Profile/Profile';
import AdminPanel from './components/pages/Admin/AdminPanel';
import Tickets from './components/pages/Tickets/Tickets';
import Cart from './components/pages/Cart/Cart';

import './App.scss';

const App = () => {
    const dispacth = useDispatch();

    const auth: { user: User; isAuth: boolean } = useSelector((state: ReducersState) => state.auth);
    const { login } = bindActionCreators(actionCreators, dispacth);

    useEffect(() => {
        if (localStorage.getItem('accessToken')) {
            axios
                .get(process.env.REACT_APP_BACKEND_URL + '/auth/autologin')
                .then((res) => {
                    login(res.data.user);
                })
                .catch((err) => {
                    console.log(err.response.data.message);
                });
        }
    }, []);

    axios.interceptors.request.use(
        (config) => {
            if (config.headers) {
                config.headers.Authorization = `Bearer ${localStorage.getItem('accessToken')}`;
                config.headers.AuthorizationRefresh = `Bearer ${localStorage.getItem('refreshToken')}`;
                config.headers.User = `userId ${localStorage.getItem('refreshToken')}`;

                return config;
            }
        },
        (error) => {
            return Promise.reject(error);
        },
    );

    return (
        <BrowserRouter>
            <NavBar />
            <Routes>
                {/* Routes for all users */}
                <Route path="/" element={<Home />} />
                <Route path="/products-page" element={<ProductsPage />} />
                <Route path="/product-page" element={<ProductPage />} />

                {/* Routes for unauthenticated users */}
                {!auth.isAuth && (
                    <>
                        <Route path="/login" element={<Login />} />
                        <Route path="/register" element={<Register />} />
                    </>
                )}

                {/* Routes for Authenticated users */}
                {auth.isAuth && (
                    <>
                        <Route path="/contact" element={<Contact />} />
                        <Route path="/profile" element={<Profile />} />
                        <Route path="/cart" element={<Cart />} />
                    </>
                )}

                {/* Routes for Admins */}
                {auth.user?.role === 'admin' && (
                    <>
                        <Route path="/admin-panel" element={<AdminPanel />} />
                        <Route path="/ticket-page" element={<Tickets />} />
                    </>
                )}

                <Route path="/*" element={<PageNotFound />} />
            </Routes>
            <Footer />
        </BrowserRouter>
    );
};

App.displayName = 'App';

export default App;

I fetch my user data in the useEffect call.

Harelk1015
  • 33
  • 4

2 Answers2

0

<PageNotFound/> component should be conditionally rendered i.e. wait for the promise to be completed then if the response from server is 404 OR the promise is rejected then only render the <PageNotFound/> component.

Another thing: You should make a <Loading/> component that is rendered till the api request is in progress and hasn't returned any response.

Guneet Thind
  • 41
  • 1
  • 6
0

Issue

The main issue is that code doesn't have any sense of any active authentication check. It doesn't even have an unauthenticated "state" to push users to log in if/when they attempt to access protected routes.

Solution

To address the logic during authentication checks, you can use a loading state. It should be initially true when the component mounts to prevent accidental route access before authentication can be confirmed, and it should be toggled true any time an authentication check is occurring.

Example:

const { login } = actionCreators;

const App = () => {
  const dispatch = useDispatch();

  const auth: { user: User; isAuth: boolean } = useSelector((state: ReducersState) => state.auth);
  const [isLoading, setIsLoading] = React.useState(true);

  useEffect(() => {
    if (localStorage.getItem('accessToken')) {
      setIsLoading(true);
      axios
        .get(process.env.REACT_APP_BACKEND_URL + '/auth/autologin')
        .then((res) => {
          dispatch(login(res.data.user));
        })
        .catch((err) => {
          console.log(err.response.data.message);
        })
        .finally(() => setIsLoading(false));
    }
  }, []);

  useEffect(() => {
    axios.interceptors.request.use(
      (config) => {
        if (config.headers) {
          config.headers.Authorization = `Bearer ${localStorage.getItem('accessToken')}`;
          config.headers.AuthorizationRefresh = `Bearer ${localStorage.getItem('refreshToken')}`;
          config.headers.User = `userId ${localStorage.getItem('refreshToken')}`;

          return config;
        }
      },
      (error) => {
        return Promise.reject(error);
      },
    );
  }, []);

  if (isLoading) {
    return null; // or loading indicator, spinner, etc...
  }

  return (
    <BrowserRouter>
      <NavBar />
      <Routes>
        ...
      </Routes>
      <Footer />
    </BrowserRouter>
  );
};

Suggestion

Abstract the auth check into authentication wrapper components that directly wrap the routes want to use the checks on. Don't conditionally render routes at all, just the protection logic to allow/prevent access. This eliminates races conditions between setting any authentication state and redirecting back to protected routes users were trying to access.

See How to Create a Protected Route for the general idea for the custom route wrappers to handle authenticated/unauthenticated "states" and allowing route access or redirecting to specific routes, like a "/login" route to sign users in.

Drew Reese
  • 165,259
  • 14
  • 153
  • 181