How can I get a component to render as a parent of protected routes in react router dom?

Viewed 100

Here is the code I am working with:

App.js

import NotFound from "components/NotFound";
import Loading from "components/Loading";
import PrivateRoute from "components/PrivateRoute";
import { logoutUser, setCurrentUser } from "features/auth/auth.slice";
import jwt_decode from "jwt-decode";
import React, { lazy, Suspense, useEffect } from "react";
import { useDispatch } from "react-redux";
import { Route, Switch, Redirect, useHistory } from "react-router-dom";
import HomeNav from "components/HomeNav";

// auth
const Login = lazy(() => import("features/auth/Login"));
const Register = lazy(() => import("features/auth/Register"));
const ForgotPassword = lazy(() => import("features/auth/ForgotPassword"));
const ResetPassword = lazy(() => import("features/auth/ResetPassword"));
// recipe
const Home = lazy(() => import("features/recipes/Home"));
const MyLikes = lazy(() => import("features/recipes/MyLikes"));
const MyRecipes = lazy(() => import("features/recipes/MyRecipes"));
const CreateRecipe = lazy(() => import("features/recipes/CreateRecipe"));
const EditRecipe = lazy(() => import("features/recipes/EditRecipe"));
const RecipeDetail = lazy(() => import("features/recipes/RecipeDetail"));
const UserProfile = lazy(() => import("features/recipes/UserProfile"));
const TopRecipes = lazy(() => import("features/recipes/TopRecipes"));
// user
const MyProfile = lazy(() => import("features/user/MyProfile"));

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

  // Check if user is already logged in
  useEffect(() => {
    const token = localStorage.token;
    // Check for token to keep user logged in
    if (token) {
      try {
        // Decode token and get user info and exp
        const { exp, ...rest } = jwt_decode(token);
        // Set user and isAuthenticated
        dispatch(setCurrentUser(rest));
        // Check for expired token
        const currentTime = Date.now() / 1000; // to get in milliseconds
        if (exp < currentTime) {
          // Logout user
          dispatch(logoutUser());
          // Redirect to login
          history.push("/");
        }
      } catch {
        // Logout user
        dispatch(logoutUser());
        // Redirect to login
        history.push("/");
      }
    }
  }, []);

  const authRoutes = [
    {
      path: "/",
      Component: Login,
    },
    {
      path: "/register",
      Component: Register,
    },
    {
      path: "/forgotpassword",
      Component: ForgotPassword,
    },
    {
      path: "/resetpassword/:resetToken",
      Component: ResetPassword,
    },
  ];

  const privateRoutes = [
    {
      path: "/home",
      Component: Home,
    },
    {
      path: "/top-3",
      Component: TopRecipes,
    },
    {
      path: "/my-likes",
      Component: MyLikes,
    },
    {
      path: "/my-recipes",
      Component: MyRecipes,
    },
    {
      path: "/profile",
      Component: MyProfile,
    },
    {
      path: "/create",
      Component: CreateRecipe,
    },
    {
      path: "/recipe/:recipeId",
      Component: RecipeDetail,
    },
    {
      path: "/update/:recipeId",
      Component: EditRecipe,
    },
    {
      path: "/profile/:username",
      Component: UserProfile,
    },
  ];

  return (
    <Suspense fallback={<Loading />}>
      <Switch>
        {/* auth routes */}
        {authRoutes.map(({ path, Component }, i) => (
          <Route exact path={path} component={Component} key={i} />
        ))}
        {/* private routes */}
        {privateRoutes.map(({ path, Component }, i) => (
          <PrivateRoute exact path={path} Component={Component} key={i} />
        ))}
        {/* not found */}
        <Route path="/404" component={NotFound} />
        <Redirect to="/404" />
      </Switch>
    </Suspense>
  );
};

export default App;

PrivateRoute.js

import React from "react";
import { Route, Redirect, useLocation } from "react-router-dom";
import { useSelector } from "react-redux";
import HomeNav from "components/HomeNav";

const PrivateRoute = ({ Component, ...rest }) => {
  const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
  const { pathname } = useLocation();

  return (
    <Route
      {...rest}
      render={(props) =>
        isAuthenticated ? (
          <HomeNav>
            <Component {...props} />
          </HomeNav>
        ) : (
          <Redirect to={{ pathname: "/", from: pathname }} />
        )
      }
    />
  );
};

export default PrivateRoute;

This is the result I was looking for in that it renders the HomeNav component as a parent on all protected routes, but it causes a rerender each time a new link is visited. In addition, if I just wrap the PrivateRoutes inside of App.js in a HomeNav, it also works, but does not properly show the Not Found page. Any help would be appreciated!

1 Answers

You can render a single private Route component that specifies an array of private paths and render the HomeNav as a wrapper here and then render another Switch with the actual private paths.

App

<Switch>
  {/* auth routes */}
  {authRoutes.map(({ path, Component }, i) => (
    <Route exact path={path} component={Component} key={i} />
  ))}

  {/* private routes */}
  <PrivateRoute path={privateRoutes.map(({ path }) => path)}>
    <HomeNav>
      <Switch>
        {privateRoutes.map(({ path, Component }, i) => (
          <PrivateRoute exact path={path} Component={Component} key={i} />
        ))}
      </Switch>
    </HomeNav>
  </PrivateRoute>
  
  {/* not found */}
  <Route path="/404" component={NotFound} />
  <Redirect to="/404" />
</Switch>

And of course you will need to remove HomeNav from PrivateRoute

const PrivateRoute = ({ Component, ...rest }) => {
  const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
  const { pathname } = useLocation();

  return (
    <Route
      {...rest}
      render={(props) =>
        isAuthenticated ? (
          <Component {...props} />
        ) : (
          <Redirect to={{ pathname: "/", from: pathname }} />
        )
      }
    />
  );
};
Related