5

I have constructed a Public route component for logging in to show up if the user is not authenticated. Whenever a user that is not logged clicks on a protected route, he will be redirected to the login page where he can enter the credentials. I want a programmatic way so that if he logged in with the correct credentials, he should be redirected to the page that he tried to access at the first place. For example if the user requested the profile page, he should be redirected to it after logging in, if the user requested the settings page, the same would happen.

As of currently, I can only redirect them to the home path /. Is there any way I can use Redirect so that it knows the path the user requested?

Here is my current code for the Public Route component

export const PublicRoute = ({
    isAuthenticated,
    component: Component,
    ...rest
}: PublicRouteProps) => (
    <Route
        {...rest}
        component={(props: any) => {
            console.log(props.path);
            return isAuthenticated.auth ? (
                <Redirect to='/' />
            ) : (
                <div>
                    <Component {...props} />
                </div>
            );
        }}
    />
);
const mapStateToProps = (state: ReduxStoreState) => ({
    isAuthenticated: state.isAuthenticated
});

export default connect(mapStateToProps)(PublicRoute);

Robin
  • 6,726
  • 4
  • 47
  • 75
Ahmed Magdy
  • 321
  • 3
  • 8

3 Answers3

10

You question cannot be answered that easily. Basically you need to remember, which path a user wanted to access, so you can redirect to that path, after the user successfully authenticated.

I've created you an example here. The explanation and some code from that example you can find below.

So if the user is not authenticated, we set the path to the app state. I would modify your ProtectedRoute to this:

import { useEffect } from 'react';
import { Redirect, Route, RouteProps, useLocation } from 'react-router';

export type ProtectedRouteProps = {
  isAuthenticated: boolean;
  authenticationPath: string;
  redirectPath: string;
  setRedirectPath: (path: string) => void;
} & RouteProps;

export default function ProtectedRoute({isAuthenticated, authenticationPath, redirectPath, setRedirectPath, ...routeProps}: ProtectedRouteProps) {
  const currentLocation = useLocation();

  useEffect(() => {
    if (!isAuthenticated) {
      setRedirectPath(currentLocation.pathname);
    }
  }, [isAuthenticated, setRedirectPath, currentLocation]);

  if(isAuthenticated && redirectPath === currentLocation.pathname) {
    return <Route {...routeProps} />;
  } else {
    return <Redirect to={{ pathname: isAuthenticated ? redirectPath : authenticationPath }} />;
  }
};

To remember the authentication and the redirection path I would create a context based on the following model:

export type Session = {
  isAuthenticated?: boolean;
  redirectPath: string;
}

export const initialSession: Session = {
  redirectPath: ''
};

According to that the context looks like this:

import { createContext, useContext, useState } from "react";
import { initialSession, Session } from "../models/session";

export const SessionContext = createContext<[Session, (session: Session) => void]>([initialSession, () => {}]);
export const useSessionContext = () => useContext(SessionContext);

export const SessionContextProvider: React.FC = (props) => {
  const [sessionState, setSessionState] = useState(initialSession);
  const defaultSessionContext: [Session, typeof setSessionState]  = [sessionState, setSessionState];

  return (
    <SessionContext.Provider value={defaultSessionContext}>
      {props.children}
    </SessionContext.Provider>
  );
}

Now you need to make this context available to your app:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './containers/App';
import { SessionContextProvider } from './contexts/SessionContext';
import { BrowserRouter } from 'react-router-dom';

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <SessionContextProvider>
        <App />
      </SessionContextProvider>
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root')
);

In your main container you can apply the protected routes:

import ProtectedRoute, { ProtectedRouteProps } from "../components/ProtectedRoute";
import { useSessionContext } from "../contexts/SessionContext";
import { Route, Switch } from 'react-router';
import Homepage from "./Homepage";
import Dashboard from "./Dashboard";
import Protected from "./Protected";
import Login from "./Login";

export default function App() {
  const [sessionContext, updateSessionContext] = useSessionContext();

  const setRedirectPath = (path: string) => {
    updateSessionContext({...sessionContext, redirectPath: path});
  }

  const defaultProtectedRouteProps: ProtectedRouteProps = {
    isAuthenticated: !!sessionContext.isAuthenticated,
    authenticationPath: '/login',
    redirectPath: sessionContext.redirectPath,
    setRedirectPath: setRedirectPath
  };

  return (
    <div>
      <Switch>
        <Route exact={true} path='/' component={Homepage} />
        <ProtectedRoute {...defaultProtectedRouteProps} path='/dashboard' component={Dashboard} />
        <ProtectedRoute {...defaultProtectedRouteProps} path='/protected' component={Protected} />
        <Route path='/login' component={Login} />
      </Switch>
    </div>
  );
};

Update March 2021

I've updated my answer above. React was throwing an error when setting the state from a foreign component. Also the previous solution didn't work when / path was not protected. This issues should be fixed.

Additionally I've created an example for React Router 6.

Robin
  • 6,726
  • 4
  • 47
  • 75
  • 1
    What if authentication comes with a delay? It redirects to the login page anyway. – Onkeltem Sep 21 '20 at 08:04
  • why not just save the token in local storage and then aceess it in the protected route component for authentication? – vikrant May 17 '21 at 09:18
  • @vikrant: This would work too, but `ProtectedRoute` won't be reusable anymore. – Robin May 17 '21 at 10:12
  • @Robin i think there is only one type of login info for the entire app generally, so all the routes can use this protected route. so in what way is it not reusable, for using in other apps as a third party package? – vikrant May 17 '21 at 10:15
  • @vikrant Yes, in this case not much speaks against to do it like you mentioned. I'm not doing this, because I don't want that components start to manipulate the local storage and try to keep the components reusable. If you have many components and some of them mess with the local storage, it's hard to have clean code. – Robin May 17 '21 at 14:37
  • @Onkeltem: Add a `isAuthenticating` state to your application. Render a route which is not protected and showing a loading indicator or so, when is authenticating. The use case "Show loading indicator when authenticating" and "Redirect to login form, if not authenticated" should not interfere in my opinion. – Robin May 17 '21 at 14:42
0

Dude i think the best ways is to use history.push function in componentDidMount like this :

componentDidMount() {
  if(isAuthenticated.auth) {
    this.props.history.push('/profile')
  }
  else {
    this.props.history.push('/login') 
  }
}
Mahdi
  • 987
  • 1
  • 7
  • 24
  • 1
    I think you misunderstood the OPs question. You need to remember the path somehow, the user wanted to access before authentication. – Robin Dec 20 '19 at 10:18
0

In your App.js

import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      urlToRedirect: null
    }
  }
  componentWillReceiveProps(nextProps) {
    if ( nextProps.location !== this.props.location && 
         !this.props.isAuthenticated && 
         this.props.history.action === 'REPLACE') {
      this.setState({urlToRedirect: this.props.location.pathname});
    }
  }
}
const mapStateToProps = (state: ReduxStoreState) => ({
    isAuthenticated: state.isAuthenticated
});
export default connect(mapStateToProps, null)(withRouter(App));

instead of using setState you can use your Redux state and then acces that URL.