Reputation: 1475
I am writing a React.js application (v15.3) using react-router (v2.8.1) and ES6 syntax. I cannot get the router code to intercept all transitions between pages to check if the user needs to login first.
My top level render method is very simple (the app is trivial as well):
render()
{
return (
<Router history={hashHistory}>
<Route path="/" component={AppMain}>
<Route path="login" component={Login}/>
<Route path="logout" component={Logout}/>
<Route path="subject" component={SubjectPanel}/>
<Route path="all" component={NotesPanel}/>
</Route>
</Router>
);
}
All the samples on the web use ES5 code or older versions of react-router (older than version 2), and my various attempts with mixins (deprecated) and willTransitionTo (never gets called) have failed.
How can I set up a global 'interceptor function' to force users to authenticate before landing on the page they request?
Upvotes: 44
Views: 99340
Reputation: 135
In React v18
and reacr-router V6
. Below is the complete structure of how I do it.
Create routes routes.tsx
import { lazy } from 'react';
const Login = lazy(() => import('../pages/Authentication/Login'));
const ProtectedPage = lazy(() => import('../pages/ProtectedPage'));
const PublicPage = lazy(() => import('../pages/PublicPage'));
const routes = [
{
path: "/Login",
element: <Login/>,
protected: false
},
{
path: "/PublicPage",
element: <PublicPage />,
protected: false
},
{
path: "/ProtectedPage",
element: <ProtectedPage />,
protected: true
}
];
export { routes };
Create AuthRequired.tsx
component to check logged in status:
import { useSelector } from 'react-redux';
import { Navigate } from 'react-router-dom';
export const AuthRequired: FC<Props> = ({ children }) => {
const { user } = useSelector((state: IRootState) => state);
const isLoggedIn = Object.keys(user).length > 0;
return isLoggedIn ? children : <Navigate to={AppRoutes.LOGIN} replace={true} />;
};
Use it in your createBrowserRouter
import { createBrowserRouter } from 'react-router-dom';
import { AuthRequired } from './AuthRequired';
import { routes } from './routes';
import App from './App';
const finalRoutes = routes.map((route) => {
return {
...route,
element:
route.protected ? (
<AuthRequired><App>{route.element}</App></AuthRequired>
) : (
<App>{route.element}</App>
),
};
});
const router = createBrowserRouter(finalRoutes);
export default router;
Then use it in your main.tsx
or file wherever you are creating your root.
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<Suspense>
<Provider store={store}>
<RouterProvider router={router} />
</Provider>
</Suspense>
</React.StrictMode>
);
Upvotes: 0
Reputation: 706
You can do it by simply creating a RestrictedRoute or Private component and pass your redux user authenticated state to this and in RestrictedRoute component redirect if state is false, in case of true redirect to component.
code:
const RestrictedRoute = ({ component: Component, authUser, auth, ...rest }) => (
<Route
{...rest}
render={props =>
auth.is_authenticated && auth.is_authorized && authUser ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: '/signin',
state: { from: props.location },
}}
/>
)
}
/>
);
<Switch>
<RestrictedRoute path={`${match.url}app`}
authUser={authUser}
auth={{ is_authenticated, is_authorized }}
component={MainApp}
/>
<Route path='/signin' component={SignIn} />
<Switch>
Upvotes: 1
Reputation: 21
This is not a safe solution
You can try n use useEffect hook on every page requiring login as:
useEffect(() => {
const token = localStorage.getItem('token');
if(!token) {
history.push('/login');
}
}
This uses useHistory hook from 'react-router-dom'
you just need to initialize it before calling it as:
const history = useHistory();
As already stated above it is not a safe sloution, but a simple one
Upvotes: 2
Reputation: 31
If you're using react-router 4 and above, use Render props and redirect to solve this. Refer: onEnter not called in React-Router
Upvotes: 3
Reputation: 647
In v4 you just create a route component that checks if uses is authenticated and return the next components and of course next component can be other routes.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Route, Redirect } from 'react-router-dom';
import AuthMiddleware from 'modules/middlewares/AuthMiddleware';
class PrivateRoute extends Component {
static propTypes = {
component: PropTypes.func.isRequired,
isAuthenticated: PropTypes.bool,
isLoggedIn: PropTypes.func.isRequired,
isError: PropTypes.bool.isRequired
};
static defaultProps = {
isAuthenticated: false
};
constructor(props) {
super(props);
if (!props.isAuthenticated) {
setTimeout(() => {
props.isLoggedIn();
}, 5);
}
}
componentWillMount() {
if (this.props.isAuthenticated) {
console.log('authenticated');
} else {
console.log('not authenticated');
}
}
componentWillUnmount() {}
render() {
const { isAuthenticated, component, isError, ...rest } = this.props;
if (isAuthenticated !== null) {
return (
<Route
{...rest}
render={props => (
isAuthenticated ? (
React.createElement(component, props)
) : (
<Redirect
to={{
pathname: isError ? '/login' : '/welcome',
state: { from: props.location }
}}
/>
)
)}
/>
);
} return null;
}
}
const mapStateToProps = (state) => {
return {
isAuthenticated: state.auth.isAuthenticated,
isError: state.auth.isError
};
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
isLoggedIn: () => AuthMiddleware.isLoggedIn()
}, dispatch);
};
export default connect(mapStateToProps, mapDispatchToProps)(PrivateRoute);
Upvotes: 7
Reputation: 1475
This version of the onEnter callback finally worked for react-router (v2.8):
requireAuth(nextState,
replace)
{
if(!this.authenticated()) // pseudocode - SYNCHRONOUS function (cannot be async without extra callback parameter to this function)
replace('/login')
}
The link which explains react-router redirection differences between V1 vs v2 is here. Relevant section quoted below:
Likewise, redirecting from an onEnter hook now also uses a location descriptor.
// v1.0.x
(nextState, replaceState) => replaceState(null, '/foo')
(nextState, replaceState) => replaceState(null, '/foo', { the: 'query' })
// v2.0.0
(nextState, replace) => replace('/foo')
(nextState, replace) => replace({ pathname: '/foo', query: { the: 'query' } })
Full Code Listing Below (react-router version 2.8.1):
requireAuth(nextState,
replace)
{
if(!this.authenticated()) // pseudocode - SYNCHRONOUS function (cannot be async without extra callback parameter to this function)
replace('/login');
}
render() {
return (
<Router history={hashHistory}>
<Route path="/" component={AppMain}>
<Route path="login" component={Login}/>
<Route path="logout" component={Logout}/>
<Route path="subject" component={SubjectPanel} onEnter={this.requireAuth}/>
<Route path="all" component={NotesPanel} onEnter={this.requireAuth}/>
</Route>
</Router>
);
}
Upvotes: 2
Reputation: 21846
Every route has an onEnter hook which is called before the route transition happens. Handle the onEnter hook with a custom requireAuth function.
<Route path="/search" component={Search} onEnter={requireAuth} />
A sample requireAuth is shown below. If the user is authenticated, transition via next(). Else replace the pathname with /login and transition via next(). The login is also passed the current pathname so that after login completes, the user is redirected to the path originally requested for.
function requireAuth(nextState, replace, next) {
if (!authenticated) {
replace({
pathname: "/login",
state: {nextPathname: nextState.location.pathname}
});
}
next();
}
Upvotes: 38