React: Private Routes Using Outlet & Router V6 - reactjs

I'm currently creating private routes using react-router 6 and Outlet. I have a simple setup as follows:
Routes in App.js
return (
<BrowserRouter>
<div className="App">
<Routes>
<Route path="/" element={<Login />}></Route>
<Route path="/register" element={<Register />}></Route>
<Route element={<PrivateRoutes />}>
<Route path="/dashboard" element={<Dashboard />}></Route>
<Route path="/reports" element={<Reports />}></Route>
<Route path="/settings" element={<Settings />}></Route>
</Route>
</Routes>
</div>
</BrowserRouter>
);
PrivateRoutes component
const PrivateRoutes = () => {
console.log("Private Routes was run");
validToken = true;
return (
(checkToken()) ? <Outlet/> : <Navigate to="/" />
);
}
What I noticed though is the PrivateRoutes component does not run when navigating between any of the private routes, only when first entering one of the PrivateRoutes.
For example, I want the PrivateRoutes component to run when I navigate between the /dashboard and /reports routes using my primary nav.
I tried using both Link and useNavigate imports from react-router-dom, but neither re-triggers the PrivateRoutes component.
<div id='nav-links' className='flex flex-row'>
<div onClick={() => navigate('/dashboard')}>Dashboard</div>
<div onClick={() => navigate('/reports')}>Reports</div>
<div onClick={() => navigate('/settings')}>Settings</div>
</div>
and
<div id='nav-links' className='flex flex-row'>
<Link to='/dashboard'>Dashboard</Link>
<Link to='/reports'>Reports</div>
<Link to='/settings'>Settings</div>
</div>
What am I missing or not understanding? Is there a simple syntax adjustment one can use so PrivateRoutes is always run upon navigation?

Your routes are fine. You will want to couple the checkToken to the route change. You can listen to route changes using the useLocation and useEffect hooks. I'm assuming checkToken is synchronous.
Example:
const PrivateRoutes = () => {
const { pathname } = useLocation();
const [isValidToken, setIsValidToken] = useState(); // <-- initially undefined
useEffect(() => {
// initial mount or route changed, check token
setIsValidToken(!!checkToken());
}, [pathname]);
if (isValidToken === undefined) {
return null; // or loading indicator/spinner/etc
}
return isValidToken ? <Outlet/> : <Navigate to="/" replace />;
}

Related

React Router V5 How to have links to nested and parent router? Adding Redux messes with Routing

I have a page which uses React Router V5. There is a general section and there is a section specifically for user profiles. As I have a different page structure there, I used nested routes for the account section. The SideBar is what the name implies and contains buttons which can be used to navigate the account pages /account/profile, /account/settings, as well as navigate to pages outside the nested switch - namely /help.
The App used to be structured roughly like this:
// in index.js
const appDiv = document.getElementById("app")
render(<App />, appDiv)
// in App.js
const App = () => {
return (
<Router>
<div className={styles.pageContainer}>
<Header />
<Switch>
<Route exact path='/' component={() => <HomePage link='about' />} />
<Route path='/about' component={AboutPage} />
<Route path='/help' component={HelpPage} />
<Route path='/log-in' component={LoginPage} />
<Route path='/sign-up/:signupType' component={SignUpPage} />
<Route path='/account' component={AccountRouter} />
</Switch>
<Footer />
</div>
</Router>
}
// in AccountRouter.js
const AccountRouter = () => {
return (
<div className={styles.container}>
<SideBar />
<Switch>
<Route path='/account/settings' component={AccountSettingsPage} />
<Route path='/account/profile' component={ProfileSettingsPage} />
<Route exact path='/account' component={ProfileSettingsPage} />
</Switch>
</div>
);
};
// in SideBar.js
const SideBar = () => {
const history = useHistory();
return (
<div>
<button onClick={() => history.push('/account/profile')}>Go to Account Profile</button>
<button onClick={() => history.push('/account/settings')}>Go to Account Settings</button>
<button onClick={() => history.push('/help')}>Go to Help Page</button>
</div>
)
}
Now it is structured like this:
// in index.js
const appDiv = document.getElementById("app")
render(
<Provider store={store}>
<App />
</Provider>
, appDiv)
// in App.js
// This looks the same
// in AccountRouter.js
// This now has Route protection
const AccountRouter = () => {
const accountData = useSelector((state) => state.account);
return (
<div className={styles.container}>
{!accountData.isLoggedIn ? <Redirect to='/log-in' /> : null}
<SideBar />
<Switch>
<Route path='/account/settings' component={AccountSettingsPage} />
<Route path='/account/profile' component={ProfileSettingsPage} />
<Route exact path='/account' component={ProfileSettingsPage} />
</Switch>
</div>
);
};
// in SideBar.js
// This looks the same.
Before I added Redux, the Sidebar was properly redirecting.
After Redux, I have the following behaviour:
When the SideBar is outside of the Switch, you can properly navigate to the Help page, but the components don't render, when I try to navigate to the pages inside the AccountRouter Switch. When I move the SideBar into the Switch, the links to the pages inside this switch start working again, but the /help link stops working.
Is there a way of having links to both inside and outside of this Switch in the same SideBar? How could Redux have affected the Router?

React router v6 nested routes can't go up one level using navigate

Target
I'm using react router v6.
I have a parent route with a static header with a back button that when I click it needs to go one path above, exp. /shop/1/item/create to /shop/1/item, when i click the back button i call the function navigate from useNavigate()
example
Red is the root page, yellow is where the static header is and green is content that i want to change
Problem
When i call navigate regardless of using "." or ".." or "./" the page never change correctly, it either redirects from /shop/1/items/create to /shop or to /shop/1
the only way of redirecting correctly is using -1 but it causes the problem that if someone copy's the URL when going back they get redirected to whatever page they were before pasting the URL.
Code
const ShopPage = () => {
const [ state, setState ] = React.useState<IShopPageState>({ shop: { name: "" }, isInvalid: false })
const { id } = useParams()
const navigate = useNavigate()
return (
<div id="shop-page">
<Card>
<div id="shop-page-header">
<Icon canHover onClick={() => navigate("./")} icon="BiArrowBack"/>
<Title text={state.shop ? state.shop.name : ""}/>
</div>
</Card>
<div id="shop-page-content">
<Routes>
<Route path="/*" element={<div onClick={() => navigate('./items')}>wddw</div>}/>
<Route path="/items/*" element={<ItemsPage shop={state.shop}/>}/>
<Route path="/items/create" element={<ItemPage/>}/>
</Routes>
</div>
</div>
)
}
Here is the code of the yellow part with only the important stuff, the header is static and always visible and where the back button is.
I believe this code is enough to understand, the green part only redirects the url from /items/ to /items/create using navigate('./create')
I think a possible solution would be simply copying pasting the header for each page but i find that to be bad practice and it is a last resort solution
Here is a example of the problem
EDIT
As asked here is some extra code showing the problem
App.js
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="*" element={<Link to="/shop">Shop</Link>} />
<Route path="shop/*" element={<Shop />} />
</Routes>
</BrowserRouter>
);
}
Shop.js
const Shop = () => {
return (
<>
<div>
<Link to="./">Back</Link>
</div>
<Routes>
<Route path="*" element={<Link to="./items">All</Link>} />
<Route path="items/*" element={<Link to="./create">Item</Link>} />
<Route path="items/create" element={<>Create</>} />
</Routes>
</>
);
};
if you are using a nested route then better to use
<Route path="/" >
<Route index element={<Items/>} />
<Route path="item:id" element={<Item/>} />
</Route>
Hey Dear #pekira you can see just a simple hint in the code below for React Router v6
import { Routes, Route, Link } from 'react-router-dom';
const App = () => {
return (
<>
<h1>React Router</h1>
<nav>
<Link to="/home">Home</Link>
<Link to="/user">User</Link>
</nav>
<Routes>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="user/:Id" element={<User />} />
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
};

How to check if user is logged by localStorage and Redirect according to it?

I am new to localStorage and React Router, and my goal is:
Redirect user to the "/dashboard" when he is logged in, and Redirect back to '/home' when he is logged out. Also, of course, not allowing him to go to the 'dashboard' if he is not logged in. For some reason my code in App.js not working:
function App() {
let userLogged;
useEffect(() => {
function checkUserData() {
userLogged = localStorage.getItem("userLogged");
}
window.addEventListener("storage", checkUserData);
return () => {
window.removeEventListener("storage", checkUserData);
};
}, []);
return (
<div className="App">
<React.StrictMode>
<Router>
<Routes>
<Route path="/" element={<Home />} />
{userLogged ? (
<Route path={"/dashboard"} element={<Dashboard />} />
) : (
<Route path={"/home"} element={<Home />} />
)}
</Routes>
</Router>
</React.StrictMode>
</div>
);
}
export default App;
I set it in the home and dashboard pages by localStorage.setItem('userLogged', false) and localStorage.setItem('userLogged', true)
You can only listen to changes in localStorage from other window/browser contexts, not from within the same browser/window context. Here it's expected the window knows its own state. In this case, you actually need some React state.
Convert the userLogged to a React state variable and use a useEffect hook to initialize and persist the userLogged state to/from localStorage. Instead of conditionally rendering Route components, create a wrapper component to read the userLogged value from localStorage and conditionally render an Outlet for nested/wrapped routes or a Navigate component to redirect to your auth route to login.
Example:
import { Navigate, Outlet, useLocation } from 'react-router-dom';
const AuthWrapper = () => {
const location = useLocation(); // current location
const userLogged = JSON.parse(localStorage.getItem("userLogged"));
return userLogged
? <Outlet />
: (
<Navigate
to="/"
replace
state={{ from: location }} // <-- pass location in route state
/>
);
};
...
function App() {
const [userLogged, setUserLogged] = useState(
JSON.parse(localStorage.getItem("userLogged"))
);
useEffect(() => {
localStorage.setItem("userLogged", JSON.stringify(userLogged));
}, [userLogged]);
const logIn = () => setUserLogged(true);
const logOut = () => setUserLogged(false);
return (
<div className="App">
<React.StrictMode>
<Router>
<Routes>
<Route path="/" element={<Home logIn={logIn} />} />
<Route path={"/home"} element={<Home logIn={logIn} />} />
<Route element={<AuthWrapper />}>
<Route path={"/dashboard"} element={<Dashboard />} />
</Route>
</Routes>
</Router>
</React.StrictMode>
</div>
);
}
export default App;
Home
import { useLocation, useNavigate } from 'react-router-dom';
const Home = ({ logIn }) => {
const { state } = useLocation();
const navigate = useNavigate();
const loginHandler = () => {
// authentication logic
if (/* success */) {
const { from } = state || {};
// callback to update state
logIn();
// redirect back to protected route being accessed
navigate(from.pathname, { replace: true });
}
};
...
};
You can render both route and use Navigate component to redirect. Like this -
// [...]
<Route path={"/dashboard"} element={<Dashboard />} />
<Route path={"/home"} element={<Home />} />
{
userLogged ?
<Navigate to="/dashboard" /> :
<Navigate to="/home" />
}
// other routes
Whenever you logout, you need to manually redirect to the desired page using useNavigate hook.

React - display specific content based on URL using useLocation

Trying to teach myself react and stuck on one part... I can't seem to get page specific content to display based on URL using useLocation() -- HELP!
App.js - router displays page on click, yay!
<Router>
<Routes>
<Route exact path="/" element={<Home />} />
<Route path="/project/projectOne" element={<Project />} />
<Route path="/project/projectTwo" element={<Project />} />
</Routes>
</Router>
Project.js - Project template serves up the components as expected
const Project = () => {
return (
<div className='content-wrapper'>
<Scroll />
<ProjectIntro />
<ProjectContent />
<ProjectGrid />
<Contact />
</div>
); }; export default Project;
ProjectIntro.js - A component trying to serve up the content -- this is where I'm stuck, useLocation() see's the path, but I can't figure out how to show the "projectIntroDetails" based on that path.
const projectOne = () => {
<h1 className='project-intro-heading'>Title Here</h1>,
<figure className='project-intro-image'>
<img src={projectImage} alt='placeholder'/>
</figure>
}
const projectTwo = () => {
<h1 className='project-intro-heading'>Title Here</h1>,
<figure className='project-intro-image'>
<img src={projectTwoImage} alt='placeholder' />
</figure>
}
const projectIntroDetails = {
projectOne: {
component: <projectOne />
},
projectTwo: {
component: <projectTwo />
}
}
const ProjectIntro = () => {
const projectPath = useLocation();
console.log(projectPath);
// this is where I need help
// how do I turn the path into seeing details to render the correct content?
const projectIntroDetail = projectIntroDetails[projectPath.pathname.split("/project/")];
return (
<div className='project-intro'>
{projectIntroDetail}
</div>
);
}; export default ProjectIntro;
You can use a component with a switch statement to determine which child component to render. This method allows you to pass any additional props to the child components.
If you don't need the <div className='project-intro'> element, you could also render the switch directly inside your ProjectIntro component.
const ProjectOne = () => {
<h1 className='project-intro-heading'>Title Here</h1>,
<figure className='project-intro-image'>
<img src={projectImage} alt='placeholder'/>
</figure>
}
const ProjectTwo = () => {
<h1 className='project-intro-heading'>Title Here</h1>,
<figure className='project-intro-image'>
<img src={projectTwoImage} alt='placeholder' />
</figure>
}
const ProjectIntros = ({ slug, ...props }) => {
switch(slug) {
case 'projectOne':
return <ProjectOne {...props} />;
case 'projectTwo':
return <ProjectTwo {...props} />;
default:
return null;
}
}
const ProjectIntro = () => {
const projectPath = useLocation();
console.log(projectPath);
return (
<div className='project-intro'>
<ProjectIntros slug={projectPath.pathname.split("/")[2]} />
</div>
);
}; export default ProjectIntro;
You don't really need to use the useLocation hook or pathname value to handle any conditional rendering logic, that's what the routing components are for.
I would suggest either passing in the correct sub-project component as a prop to be rendered on the correctly matching route, or refactoring the routes to do this in a more "react router" way.
Passing component down as prop example:
App
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/project/projectOne"
element={<Project projectIntro={<ProjectOne />} />}
/>
<Route
path="/project/projectTwo"
element={<Project projectIntro={<ProjectTwo />} />}
/>
</Routes>
</Router>
Project
const Project = ({ projectIntro }) => {
return (
<div className='content-wrapper'>
<Scroll />
<div className='project-intro'>
{projectIntro}
</div>
<ProjectContent />
<ProjectGrid />
<Contact />
</div>
);
};
Using react-router-dom to your advantage.
Project
Convert Project into a layout component and render the ProjectOne and ProjectTwo components on nested routes. Layout routes are intended to be used to share common UI elements and layout, and render routed content into an outlet.
import { Outlet } from 'react-router-dom';
const Project = () => {
return (
<div className='content-wrapper'>
<Scroll />
<div className='project-intro'>
<Outlet /> // <-- render nested routes here
</div>
<ProjectContent />
<ProjectGrid />
<Contact />
</div>
);
};
App
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/project" element={<Project />}>
<Route path="projectOne" element={<ProjectOne />} />
<Route path="projectTwo" element={<ProjectTwo />} />
</Route>
</Routes>
</Router>

React router show only route without components defined in my browser

Hello in my directions file I set my struct
header
navbar
and my switch
foote
const AppRouter = () => (
<BrowserRouter>
<Route path="/login" component={AuthPage} exact={true} />
<Route path="/dashboard/addProduct" component={AddProduct} exact={true} />
<div>
<Header/>
<Navigation/>
<Container maxWidth="lg" >
<Switch>
<Route path="/" component={LandingPage} exact={true} />
<Route path="/xd" component={AuthPage} exact={true} />
<Route component={NotFoundPage} />
</Switch>
</Container>
</div>
</BrowserRouter>
);
But I have two routes where I didn't want to show my header
footer
and nav bar
which are the login and addproduct routes
how could i do that?
This is a little bit hacky (just to match your current approach) since I'm assuming you just want to hide those components in only those 2 routes, You can use withRouter in your Header and Navigation components, withRouter will provide to you the location prop and you can have a condition like this:
import React from "react";
import { withRouter } from "react-router-dom";
const Navigation = props => {
const { location } = props;
const { pathname } = location;
if (pathname !== "/login" || pathname !== "/dashboard/addProduct" ) {
return (
// Component logic goes here
);
}
// if we're on those routes we should return null to not render anything
return null;
};
export default withRouter(Navigation);
If you want a more robust long term approach this answer could help:
How to hide navbar in login page in react router

Resources