skip to Main Content

I have a following Router component:

Router.js

const Router = () => {
  const router = createBrowserRouter(RouteList);
  return (
    <UserProfileProvider>
      <RouterProvider router={router} />
    </UserProfileProvider>
  );
};

export default Router;

routes.js

function withSuspense(Component) {
  return (
    <SuspenseWrapper>
      <Component />
    </SuspenseWrapper>
  );
}
    
const RouteList = [
  {
    path: HomePath,
    element: withSuspense(SharedView),
    errorElement: withSuspense(ErrorView),
    children: [
      {
        index: true,
        element: withSuspense(HomeView),
      },
      {
        path: ManageRolePath,
        children: [
          {
            index: true,
            element: withSuspense(ManageRole),
          },
          {
            path: ManageRoleId,
            element: withSuspense(ManageRoleInfo),
          },
        ],
      },
      {
        path: AssignRoleToUserPath,
        children: [
          {
            index: true,
            element: withSuspense(AssignRoleToUsers),
          },
          {
            path: AssignUserId,
            element: withSuspense(UserInfo),
          },
        ],
      },
    ],
  },

];

export default RouteList;
const SuspenseWrapper = ({ children }) => {
  const { availableScreens } = useContext(UserContext);
  const location = useLocation();
  const navigate = useNavigate();

  const currentPath = location?.pathname?.split("/")[1];

  useEffect(() => {
    const hasAccessToPage = checkAccesstoPage(currentPath, availableScreens);

    if (!hasAccessToPage && availableScreens?.length > 0) {
      navigate("/");
    }
  }, [currentPath, availableScreens, navigate]);

  return (
    <Suspense fallback={<Loader loadingText="Preparing menu" />}>
      {children}
    </Suspense>
  );
};

export default SuspenseWrapper;

Now here, when I am on the "localhost:3000" to goes to the home page. Now by using navigation, I am redirecting to a respective page. I am trying to implement a functionality , where if user copy the url and paste it in the browser and if user does not have the access of the page then it should redirect to the home page.

In this case, API response I am setting in the context. Right now, when I do this then it still loads the element with respective to that route.

How do I fix this? I want to element: withSuspense(ManageRole) render this only if it has the access. On first load it is still showing the route element.

2

Answers


  1. Basically you should wait until you’ve confirmed a user has correct permissions before rendering the protected content. The SuspenseWrapper component unconditionally renders its children prop. Refactor the SuspenseWrapper component to conditionally return the protected content or the redirect, based on access.

    Example:

    const SuspenseWrapper = ({ children }) => {
      const { availableScreens } = useContext(UserContext);
      const { pathname } = useLocation();
    
      const currentPath = pathname.split("/")[1];
    
      const hasAccessToPage = checkAccesstoPage(currentPath, availableScreens);
    
      return !hasAccessToPage && !!availableScreens?.length
        ? <Navigate to="/" replace />
        : (
          <Suspense fallback={<Loader loadingText="Preparing menu" />}>
            {children}
          </Suspense>
        );
    };
    

    It should be noted though that using Higher-Order-Components has fallen a bit out of favor. You also seem to be over-complicating the code just a bit. Instead of wrapping each routed component you could, and should, probably convert the SuspenseWrapper into a couple layout route components, one to handle the suspense (e.g. the dynamic imports) and the other to handle route protection.

    Example:

    import { Navigate, Outlet } from 'react-router-dom';
    
    const PrivateRoutes = () => {
      const { availableScreens } = useContext(UserContext);
      const { pathname } = useLocation();
    
      const currentPath = pathname.split("/")[1];
    
      const hasAccessToPage = checkAccesstoPage(currentPath, availableScreens);
    
      return !hasAccessToPage && !!availableScreens?.length
        ? <Navigate to="/" replace />
        : <Outlet />;
    };
    
    const RouteList = [
      {
        element: <PrivateRoutes />,
        children: [
          {
            path: HomePath,
            element: <SharedView />,
            errorElement: <ErrorView />,
            children: [
              { index: true, element: <HomeView /> },
              {
                path: ManageRolePath,
                children: [
                  { index: true, element: <ManageRole /> },
                  { path: ManageRoleId, element: <ManageRoleInfo /> },
                ],
              },
              {
                path: AssignRoleToUserPath,
                children: [
                  { index: true, element: <AssignRoleToUsers /> },
                  { path: AssignUserId, element: <UserInfo /> },
                ],
              },
            ],
          },
        ],
      }
    ];
    
    const Router = () => {
      const router = createBrowserRouter(RouteList);
      return (
        <Suspense fallback={<Loader loadingText="Preparing menu" />}>
          <UserProfileProvider>
            <RouterProvider router={router} />
          </UserProfileProvider>
        </Suspense>
      );
    };
    

    The Data Routers also support lazily loading routes as well, something like the following:

    const RouteList = [
      {
        element: <PrivateRoutes />,
        children: [
          {
            path: HomePath,
            element: <SharedView />,
            errorElement: <ErrorView />,
            children: [
              { index: true, lazy: () => import("./pages/HomeView") },
              {
                path: ManageRolePath,
                children: [
                  {
                    index: true,
                    lazy: () => import("./pages/ManageRole"),
                  },
                  {
                    path: ManageRoleId,
                    lazy: () => import("./pages/ManageRoleInfo"),
                  },
                ],
              },
              {
                path: AssignRoleToUserPath,
                children: [
                  {
                    index: true,
                    lazy: () => import("./pages/AssignRoleToUsers"),
                  },
                  {
                    path: AssignUserId,
                    lazy: () => import("./pages/UserInfo"),
                  },
                ],
              },
            ],
          },
        ],
      }
    ];
    
    const Router = () => {
      const router = createBrowserRouter(RouteList);
      return (
        <UserProfileProvider>
          <RouterProvider
            router={router}
            fallbackElement={<Loader loadingText="Preparing menu" />}
          />
        </UserProfileProvider>
      );
    };
    
    Login or Signup to reply.
  2. So If I understand this correctly, (correct me if am wrong), what you want to do is whenever someone lands on your site your app should first check if a user is eligible to go to home page or they should be sent back to login page, so this is how I used to tackle this situation

    My Router –

     return (
        <Router>
          <Routes>
            <Route element={<Auth />}>
              <Route
                path="/"
                element={(
                  <React.Suspense fallback={<Loading />}>
                    <Login />
                  </React.Suspense>
              )}
              />
              <Route
                path="/:FileId"
                element={(
                  <React.Suspense fallback={<Loading />}>
                    <Dashboard />
                  </React.Suspense>
              )}
              />
            </Route>
    
          </Routes>
        </Router>
      );
    

    So here I had two routes in my app one was the login route and another was simply the page where files were used to render this was a Notion clone, Let’s say someone directly tried to land on the file page using the URL that contains the id of that file so to tackle this i wrapped my routes in a parent route called outlet, so whenever anyone lands on any of these routes first the parent route or outlet will run

    Here is the code of the outlet element i.e Auth component

    import {
      useEffect, useState, React,
    } from 'react';
    import { Outlet, useNavigate } from 'react-router-dom';
    import Login from '../Pages/Login/Login';
    import Loading from '../Components/Loading/Loading';
    
    function Auth() {
      const Navigate = useNavigate();
    
      const [AuthCheck, setAuthCheck] = useState(null);
      const url = import.meta.env.VITE_URL;
      const [LastVisitiedFileId, setLastVisitiedFileId] = useState(null);
    
      const GetLastVisitedFileId = async () => {
        const res = await fetch(`${url}/lastvisitedfile`, {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
            'x-auth-token': localStorage.getItem('token'),
          },
        });
        const data = await res.json();
        if (data.status === 200) {
          setLastVisitiedFileId(data.data);
          localStorage.setItem('LastVisitedFileId', data.data);
        } else {
          console.log('something went wrong');
        }
      };
    
      const PrivateRoutes = async () => {
        const res = await fetch(`${url}/verify_route`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'x-auth-token': localStorage.getItem('token'),
          },
        });
        const data = await res.json();
    
        if (data.status === 201) {
          const FileId = localStorage.getItem('LastVisitedFileId');
          if (FileId === '' || FileId === null || FileId === undefined) {
            await GetLastVisitedFileId();
          } else {
            setLastVisitiedFileId(FileId);
          }
          setAuthCheck(true);
        } else {
          setAuthCheck(false);
        }
      };
    
      // made a use effect here so that whenever this file is invoked through app.js then
      // this function must runs  otherwise it will have the default values in it
    
      useEffect(() => {
        const token = localStorage.getItem('token');
        if (token === '' || token === undefined) {
          setAuthCheck(false);
        } else {
          PrivateRoutes();
        }
      });
    
      // useEffect(() => {
    
      // }, [AuthCheck]);
    
      return (
        <div>
          {
                AuthCheck === true
                  ? window.location.pathname === '/'
                    ? Navigate(`/${LastVisitiedFileId}`)
                    : <Outlet />
                  : AuthCheck === false
                    ? <Login />
                    : <Loading />
                }
        </div>
      );
    }
    
    export default Auth;
    

    This code can look a little big at first sight but I’ll break it up for you, so first thing’s first you don’t need to understand the GetLastVisitedFileId function it was made according to my need, so lets just focus on main logic,

    Overview

    So the first thing that will happen after our application enters this auth component is render something, because that’s how it works, you need to render something whenever a component runs only after that all the functions and useeffects are checked. So if you check the return statement which contains the rendering markup i am checking the value of AuthCheck now AuthCheck value is not updated yet because no function or use effect has ran yet, so to tackle this i set the initial value of AuthCheck as null, and in my rendering logic i mentioned that whenever AuthCheck is null just render the loading component , it only contains a loader, now once that is rendered now our useeffect can run ,

    Authentication Logic

    Now for authentication i was using JWT token so if a new user has just came for first time or there JWT is deleted from there storage then obviously there won’t be any JWT token in the storage, so i was checking if JWT is empty then just set the value of AuthCheck to false and hence login page gets rendered, else run the Verification function which checks if the Token is correct and validates it , if token gets validated and i get a 200 response from backend then i used to set the value of AuthCheck to be true and the corresponding children (Will always be Dashboard component in this case beacause there were not some other routes) used to get rendered.

    Let me know if you did not understood something

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