React router private route with local storage? - reactjs

I have a private route component which I use in my app.
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
const PrivateRoute = ({ component: Component, ...rest }) => {
const userLoggedIn = localStorage.getItem('token');
return (
<Route
{...rest}
render={(props) => (
userLoggedIn
? <Component {...props} />
: (
<Redirect to={{
pathname: '/login',
}}
/>
)
)}
/>
);
};
PrivateRoute.propTypes = {
component: PropTypes.elementType.isRequired,
};
export default PrivateRoute;
When logging in I'm setting token to localStorage and redirecting to PrivateRoute. The problem is that here userLoggedIn is null although if I check DevTools token is there in localStorage.
I'm not sure what to do here. I'm using localStorage so that when the page is refreshed user still logged in. All the help will be appreciated.
Parent of Private Route
const Routes = () => (
<>
<Provider store={store}>
<HeaderContainer />
<Switch>
<>
<PrivateRoute path="/" component={HomeContainer} exact />
</>
</Switch>
</Provider>
</>
);
export default Routes;
App.js
const App = () => (
<div className={styles.App}>
<Routes />
</div>
);

According to their documentation:
All children of a <Switch> should be <Route> or <Redirect> elements.
Only the first child to match the current location will be rendered.
It looks like your Private Route is wrapped in a React fragment, which could be causing the issue. Does moving the routes outside the fragment fix things?
<Switch>
{/* <> // remove line */}
<PrivateRoute path="/" component={HomeContainer} exact />
{/* </> // remove line */}
</Switch>

Related

How to make header component know if user is logged in in React?

I have a React project that has a HeaderComponent that exists for all routes in project like this:
function App() {
return (
<Fragment>
<Router>
<HeaderComponent />
<Routes>
<Route path="/login" element={<Login />}></Route>
<Route path="/register" element={<Register />}></Route>
<Route path="/" element={<LandingPage />}></Route>
</Routes>
<FooterComponent />
</Router>
</Fragment>
);
}
And my problem is that the <HeaderComponent> is rendered when the website first loads but when the user logs in, the <HeaderComponent> is not aware of the changes because the component has already mounted.
So in my <HeaderComponent>, the componentDidMount function looks like this:
componentDidMount() {
AuthService.authorizeUser()
.then((r) => {
this.setState({ loggedIn: true });
})
.catch((error) => {
this.setState({ loggedIn: false });
});
}
This only works if I refresh the page.
Basically, if a user successfully logs in (from the <Login> component), what is the proper way of making my HeaderComponent aware of this?
You can use Context API to make AuthContext to share global state within your app:
// AuthContext.js
export const AuthContext = React.createContext({});
export const AuthProvider = ({
children,
}) => {
// your context logic
return (
<AuthContext.Provider value={yourAuthValue}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => React.useContext(AuthContext);
// Layout.js
import { Outlet } from 'react-router-dom'
// Using `Outlet` to render the view within layout
export const Layout = () => {
return (
<>
<HeaderComponent />
<Outlet />
<FooterComponent />
</>
)
}
// HeaderComponent.js
import { useAuth } from './AuthContext'
export const HeaderComponent = () => {
// get state from auth context
const { isLoggedIn } = useAuth()
return // rest of your code
}
// App.js
function App() {
return (
<Fragment>
<-- Wrap your app with AuthContext let other components within your app can access auth state !-->
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<LandingPage />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
</Route>
</Routes>
</BrowserRouter>
</AuthProvider>
</Fragment>
);
}
There are a couple of ways to do so.
When you're facing a situation where you need to share the same state between multiple components, lifting the state up should be the first thing to try Check this codesandbox.
And some great blogposts to read, KCD - Prop Drilling, KCD - State Management with React
Such approach may cause "prop drilling" when you need the same state in deeply nested components and that's where the context API comes in handy.
codesandbox

For different route, same react component does not get mounted

I am having an issue when using same component for two different routes, where i am expecting that that component gets destroyed and than get mounted again, but that does not happen:
When i change from /page1 to /page2 by clicking on the button Change to /page2 output in the console should be:
COMPONENT DISMOUNTED
COMPONENT MOUNTED
This means that MyComponent should be destroyed after path changes. This is important because i rely on the fact that change of the path gives me fresh component. I don't want to reset states and other hooks to default values manually.
Codesadnbox example
Is there a React problem or perhaps React router one?
App component
import {
Routes,
Route,
BrowserRouter,
Navigate
} from 'react-router-dom';
const App = () => {
return (
<BrowserRouter>
{/* Routes */}
<Routes>
{/* Route 1 */}
<Route path="/page1" element={<MyComponent someProp="value1" />} />
{/* Route 2 */}
<Route path="/page2" element={<MyComponent someProp="value2" />} />
<Route path="/*" element={<Navigate to={{ pathname: '/page1' }} />} />
</Routes>
</BrowserRouter>
);
};
MyComponent
import type { FunctionComponent } from 'react';
import { useEffect } from 'react';
import {
useNavigate
} from 'react-router-dom';
const MyComponent: FunctionComponent<{ someProp: string }> = ({ someProp }) => {
const history = useNavigate();
const onRouteChange = (route: string) => {
history(route);
};
useEffect(() => {
console.log('COMPONENT MOUNTED');
return () => {
console.log('COMPONENT DISMOUNTED');
};
}, []);
return (
<div>
<button onClick={() => onRouteChange('/page1')}>Change to /page1</button>
<button onClick={() => onRouteChange('/page2')}>Change to /page2</button>
<div>{someProp}</div>
</div>
);
};
React is actually doing its job correctly, since Route component returns same component with changed prop someProp. In any other case where i have a component where i change prop to it, this would happen again.
There is no obvious way to find this out unless you stumble upon this problem. Although thinking in the way React works, this should be obvious.
SOLUTION
Simple key should be added to both MyComponent components. In this way, React will know, because of the different key, that new component returned by Route differs.
Codesandbox to the solution
const App = () => {
return (
<BrowserRouter>
{/* Routes */}
<Routes>
{/* Route 1 */}
<Route
path="/page1"
element={<MyComponent key="/page1" someProp="value1" />}
/>
{/* Route 2 */}
<Route
path="/page2"
element={<MyComponent key="/page2" someProp="value2" />}
/>
<Route path="/*" element={<Navigate to={{ pathname: "/page1" }} />} />
</Routes>
</BrowserRouter>
);
};

Infinite loop with React router

I am trying to figure out how to resolve an infinite loop issue.
FLOW: I have a state hook setUserData that I am passing to HomePage. From HomePage I am calling API to retrieve data and then set it in the App component.
Now when data is set and I click back button, then it into an infinite loop because userData is already set.
What can I do to fix it ?
CODE OF INTEREST:
{userData ?
<Redirect push
to={{
pathname: "/user",
search: `username=${userData.data.login}`
}}
/>
:
<Homepage setUserData={setUserData} />}
import React, { useState } from "react";
import "./App.css";
import Homepage from "./components/Homepage";
import ResultsPage from "./components/ResultsPage";
import Footer from "./components/Footer"
import { HashRouter, Switch, Route } from "react-router-dom";
import { Redirect } from "react-router-dom";
function App() {
const [userData, setUserData] = useState(null);
return (
<div className="App">
<HashRouter>
<Switch>
<Route exact path="/">
{userData ?
<Redirect push
to={{
pathname: "/user",
search: `username=${userData.data.login}`
}}
/>
:
<Homepage setUserData={setUserData} />}
</Route>
<Route path="/user">
<ResultsPage userData={userData} />
</Route>
</Switch>
</HashRouter>
{/* <Footer /> */}
</div>
);
}
export default App;
Base URL points to HomePage. When search is done on HomePage, data is passed to ResultsPage. (This is happening currently). Issue is that once userData is populated then clicking back redirects to ResultPage again. I need to be able to come back to HomePage when user clicks back button on home browser (Even if data is already loaded).
if u want achieved it, the better way is handle redirect in your homepage file
assume that
HomePage.jsx
import { useHistory} from "react-router-dom";
const HomePage = (props)=>{
const { setUserData } = props
....
const history = useHistory()
const handleClick = ()=>{
const data = HTTP.get('api')
setUserData(data)
history.push(`/user?username=${data}`)
}
...
return (
...
<button onClick={handleClick} >BUTTON</button>
...
)
}
App.jsx
function App() {
const [userData, setUserData] = useState(null);
return (
<div className="App">
<HashRouter>
<Switch>
<Route exact path="/">
<Homepage setUserData={setUserData} />}
</Route>
<Route path="/user">
<ResultsPage userData={userData} />
</Route>
</Switch>
</HashRouter>
{/* <Footer /> */}
</div>
);
}

Redirect and show the component if user is logged in in React

I have a login page which is a public route and a dashboard page which is a private route as follows:
App.js:
function App() {
return (
<Router>
<div className="App">
<Switch>
<PublicRoute path="/" exact component={Login} />
<PrivateRoute path="/dashboard" component={Dashboard} />
</Switch>
</div>
</Router>
);
}
And this is my private route component:
let store = require("store");
const PrivateRoute = ({component: Component, ...options}) => {
// const finalComponent = user != null && user.auth == true ? component : Login;
const [userData, setUserData] = useState({});
useEffect(() => {
setUserData(store.get("userData"));
}, []);
return (
<Route
{...options}
render={(props) =>
store.get("userData") ?
<Component {...props} />
:
<Redirect to="/"/>
}
/>
);
};
export default PrivateRoute;
Now the problem that I am facing is that if I open the dashboard localhost:9999/dashboard none of my components are visible because they are routed inside the dashboard component.
So when I open this localhost:9999/dashboard I want react to redirect to localhost:9999/dashboard/home and also show the components if the user is logged in.
I tried to do this :
return (
<Route
{...options}
render={(props) =>
store.get("userData") ?
<Redirect to="/dashboard/home" component={<Component {...props}} />
:
<Redirect to="/"/>
}
/>
);
This did route to the home page but nothing was visible at all. I mean the components inside dashboard weren't visible at all as well.
This is what I am getting. You see it redirects but doesn't show anything:
I believe that you could be more specific about which file is the code that you are showing.
But I feel that what you are trying to accomplish something like this:
PrivateRouter
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
const PrivateRoute = ({component: Component, restricted, ...rest}) => {
return (
<Route {...rest} render={props => (
user.auth == true ?
<Redirect to="/dashboard" />
: <Redirect to="/dashboard/home" />
)} />
);
};
export default PrivateRoute;

React Router Wrap Multiple Routes in a Redirect

Given an authentication token and a function checkToken how would I go about rerouting from multiple routes using the react router to prevent repetition like the below?
<Route exact path="/" render={() => {
return checkToken() ? (<Dashboard />) : (<Redirect to="/login" />)
}} />
<Route exact path="/about" render={() => {
return checkToken() ? (<About />) : (<Redirect to="/login" />)
}} />
It gets cumbersome if I have a couple dozen routes to have this repeated.
Surely there must be a better way!
Here is how I like to handle this:
Create a routers folder in src
Inside the router folder create 3 files AppRouter.js, PrivateRoute.js & PublicRoute.js
Here is your PublicRoute.js:
import React from 'react';
import { connect } from 'react-redux';
import { Route, Redirect } from 'react-router-dom';
export const PublicRoute = ({ isAuthenticated, component: Component, ...rest }) => (
<Route {...rest} component={(props) => (
isAuthenticated ? <Redirect to="/dashboard" /> : <Component {...props} />
)} />
);
const mapStateToProps = state => ({
isAuthenticated: // however you need to keep track of that...
});
export default connect(mapStateToProps)(PublicRoute);
Here is your PrivateRoute.js:
import React from 'react';
import { connect } from 'react-redux';
import { Route, Redirect } from 'react-router-dom';
export const PrivateRoute = ({ isAuthenticated, component: Component, ...rest }) => (
<Route {...rest} component={(props) => (
isAuthenticated ? <Component {...props} /> : <Redirect to="/" />
)} />
);
const mapStateToProps = state => ({
isAuthenticated: // however you need to keep track of that...
});
export default connect(mapStateToProps)(PrivateRoute);
And finally here is your AppRouter.js:
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Dashboard from '../components/Dashboard';
import NotFound from '../components/NotFound';
import Login from '../components/Login';
import PrivateRoute from './PrivateRoute';
import PublicRoute from './PublicRoute';
const AppRouter = () => (
<BrowserRouter>
<Switch>
{/* use PublicRoute for public routes */}
<PublicRoute exact path="/" component={Login} />
{/* use PrivateRoute for private routes */}
<PrivateRoute path="/dashboard" component={Dashboard} />
<Route component={NotFound} />
</Switch>
</BrowserRouter>
);
export default AppRouter;
For more info on HOCs (Higher Order Components) look up the docs: https://reactjs.org/docs/higher-order-components.html
One way to achieve it is by placing your checkToken function inside componentDidMount so you will always check if your user is authenticated each time this component is mounted.
After that you can do something like this:
let routes = (
<Switch>
<Route path="/login" component={LoginComponent} />
<Redirect to="/" />
</Switch>
);
if (isAuth) {
routes = (
<Switch>
<Route path="/yourRoute" component={YourComponent} />
<Redirect to="/" />
</Switch>
);
}
return (
<div>
{routes}
</div>

Resources