How to add protected routes in react redux

Viewed 4365

I have created a login component on which I have all the logic stuff.

The login reducer is:

const openState = {
  loggedIn: null,
  user: null
}

export default (state = openState, action) => {
  switch (action.type) {
    case LOGIN:
      return { ...state, loggedIn: true, user: action.payload }
    case LOGOUT:
      return { ...state, loggedIn: false, user: null }
    default:
      return openState
  }
}

The Action :

export const logIn = (user) => {
  return {
    type: LOGIN,
    payload: user
  }
}

export const logOut = () => {
  return {
    type: LOGOUT
  }
}

everything is working just fine but I'm not sure how to pass the loggedIn and user props from action into the routes component in order to secure all routes:

const MainRoutes = props => {
  const { loggedIn } = props;

  console.log(props.loggedIn)

return (
  <Router history={history}>
    <Baseline />
    <Menu/>
    <Container maxWidth="md">
      <Switch>
        <Route exact path="/Login" component={Login} />
        <Route exact path="/Carousel" component={Carousel} />
        <Route exact path="/Stepper" component={Stepper} />
        <Route component={NotFound} />
      </Switch>
    </Container>
  </Router>
);
}

const mapStateToProps = (state) => {
return { loggedIn: state.loggedIn };
};

export default connect(mapStateToProps)(MainRoutes);

If I'll console.log the loggedIn props I get undefined :| Based on loggedIn I can create a logic into the routes component.

6 Answers

You could simply define a custom ProtectedRoute component that'll be connected to redux state. In your case it should map state.auth.loggedIn and state.auth.user to props and perform a Redirect if those values are falsy:

import React from "react";
import { Route, Redirect } from "react-router-dom";
import PropTypes from "prop-types";


const ProtectedRoute = (props) => {
  const { redirectPath, component, user, loggedIn, ...routeProps} = props;
  const Component = component;
  const isAccessible = Boolean(user) && loggedIn;

  return (
    <Route
      {...routeProps}
      render={props => {
        if (isAccessible) return <Component {...props} />;
        return <Redirect to={{ pathname: redirectPath || "/Login" }} />;
      }}
    />
  );
};

ProtectedRoute.propTypes = {
  path: PropTypes.string.isRequired,
  redirectPath: PropTypes.string,
  component: PropTypes.oneOfType([
    PropTypes.shape({ render: PropTypes.func.isRequired }),
    PropTypes.func
  ]),
};

const mapStateToProps = (state) => {
  return { 
    loggedIn: state.auth.loggedIn, 
    user: state.auth.user 
  };
};

export default connect(mapStateToProps)(ProtectedRoute);

With this in place your MainRoutes won't need connect anymore:

const MainRoutes = props => {
  return (
    <Router history={history}>
      ...
      <Container maxWidth="md">
        <Switch>
          <Route exact path="/Login" component={Login} />
          <ProtectedRoute exact path="/Carousel" component={Carousel} />
          <ProtectedRoute exact path="/Stepper" component={Stepper} />
          <Route component={NotFound} />
        </Switch>
      </Container>
    </Router>
  );
}

export default MainRoutes;

Update:

If you want to keep auth state after page refresh you'll need to perform some extra setup of your redux store. The main idea is to subscribe on every action and put a fresh copy of state into localStorage or cookies and also to persist initialState from selected storage before your app boots up. Here is an example:

function getFromStorage(key) {
  try {
    return JSON.parse(window.localStorage.getItem(key)) || {};
  } catch (err) {
    return {};
  }
}

const initialState = getFromStorage("APP_STATE");

const store = createStore(
  rootReducer,
  initialState, // should go after root reducer
  ...
);

store.subscribe(() => {
  window.localStorage.setItem("APP_STATE", JSON.stringify(store.getState()));
});

And here's the working sandbox (bootstrapped by SuleymanSah)

As others already described, you had better to create a Protected Route to accomplish what you want. You simply redirect the user to the Login route if he/she is not logged in.

Here is my implementation: (codesandbox)

import React from "react";
import { Route, Redirect } from "react-router-dom";
import { connect } from "react-redux";

const ProtectedRoute = ({
  path,
  component: Component,
  render,
  loggedIn,
  ...rest
}) => {
  return (
    <Route
      path={path}
      {...rest}
      render={(props) => {
        if (loggedIn) {
          return Component ? <Component {...props} /> : render(props);
        }
        return (
          <Redirect
            to={{
              pathname: "/login",
              state: { from: props.location }
            }}
          />
        );
      }}
    />
  );
};

const mapStateToProps = (state) => {
  const { loggedIn } = state.auth;
  return {
    loggedIn
  };
};

export default connect(mapStateToProps)(ProtectedRoute);

This approach uses the official example of react router, basically you need to connect your redux store with this ProtectedRoute component

import { connect } from "react-redux";
import { Route, Redirect } from "react-router-dom";

function PrivateRoute({ children, loggedIn, ...rest }) {
  return (
    <Route
      {...rest}
      render={({ location }) =>
        loggedIn ? (
          children
        ) : (
          <Redirect
            to={{
              pathname: "/login",
              state: { from: location }
            }}
          />
        )
      }
    />
  );
}

const mapStateToProps = (state) => {
  return { loggedIn: state.auth.loggedIn };
};

export default connect(mapStateToProps)(PrivateRoute);

As you can see you're passing the loggedIn value to this new route component, so it this is false it will return you to the login page.

In order to use it just call the component in your router:

import PrivateRoute from "./PrivateRoute";

const MainRoutes = () => {

return (
  <Router history={history}>
    <Baseline />
    <Menu/>
    <Container maxWidth="md">
      <Switch>
        <Route exact path="/Login" component={Login} />
        <PrivateRoute exact path="/Carousel" component={Carousel} />
        <PrivateRoute exact path="/Stepper" component={Stepper} />
        <Route component={NotFound} />
      </Switch>
    </Container>
  </Router>
);
}

It is a nice approach because you're not connecting the MainRoutes component to the store, which in my opinion is not something you would need in that component, you'll end up using it in the components inside the Routes.

Also you forgot to add the state.auth.loggedIn to your Main Routes mapStateToProps, that's why is undefined when you're trying to use it.

Here's a small sandbox that I did to illustrate the process: https://codesandbox.io/s/unruffled-keller-pwlib

What I would do in this scenario is to create a private route basically, which would only be accessible if you are logged in, or in your case, if the loggedIn in your loggedIn: state.loggedIn is true and if a user exists in state.auth.user. And if both are false, it will redirect you to the /login page to login or signup first before accessing those private routes.

So to create a private route or a <PrivateRoute />,

import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';


const PrivateRoute = ({ 
  component: Component,
  auth: { user, loggedIn },
  ...rest 
}) => (
  <Route 
    {...rest} 
    render={props => 
      loggedIn && Boolean(user) ? (
        <Component {...props} />  
      ) : (
        <Redirect to="/login" />
      )
    } 
  />
);

PrivateRoute.propTypes = {
  auth: PropTypes.object.isRequired
}

const mapStateToProps = state => ({
  auth: state.auth
});

export default connect(mapStateToProps)(PrivateRoute);

This will create that private route or <PrivateRoute /> for you which you can import into your MainRoutes file and then you can pass on the components to it which are supposed to be protected by the private route.

const MainRoutes = props => {
  return (
    <Router history={history}>
      <Baseline />
      <Menu/>
      <Container maxWidth="md">
        <Switch>
          <Route exact path="/Login" component={Login} />
          <PrivateRoute exact path="/Carousel" component={Carousel} />
          <PrivateRoute exact path="/Stepper" component={Stepper} />
          <Route component={NotFound} />
        </Switch>
      </Container>
    </Router>
  );
}

export default MainRoutes;

This way the components to be protected will check if the user exists and if you are logged in or not and if true, it will let you access those components.

The below code worked for me.

const SecretRouter = ({ component: Component, location, loggedIn , ...rest }) => (
  <Route
    location={location}
    {...rest}
    render={(props) =>
      loggedIn === true ? (
        <Component {...props} />
      ) : (
          <Redirect
            to={{
              pathname: "/Login"
            }}
          />
      )
    }
  />
);

const mapStateToProps = (state) => {
return { loggedIn: state.loggedIn };
};

export const PrivateRouter = connect(mapStateToProps)(
  SecretRouter
);

const MainRoutes = props => {

return (
  <Router history={history}>
    <Baseline />
    <Menu/>
    <Container maxWidth="md">
      <Switch>
        <Route exact path="/Login" component={Login} />
        <PrivateRouter exact path="/Carousel" component={Carousel} />
        <PrivateRouter exact path="/Stepper" component={Stepper} />
        <Route component={NotFound} />
      </Switch>
    </Container>
  </Router>
);
}


export default MainRoutes;

You can create a Guard which gets login status from Redux store and renders children if it is logged in, otherwise redirect to somewhere else. To get the state from the store, you can use useSelector React Hook.

import { useSelector } from 'react-redux';

function AuthGuard({children}) {
    const loggedIn = useSelector(store => store.loggedIn);
    
    if (loggedIn) {
        return children;  
    } else {
        return <Redirect to="/login" />
    }
}

and in the route

    <Container maxWidth="md">
      <Switch>
        <Route exact path="/Login" component={Login} />
        <AuthGuard>
            <Route exact path="/Carousel" component={Carousel} />
            <Route exact path="/Stepper" component={Stepper} />
        </AuthGuard>
        <Route component={NotFound} />
      </Switch>
    </Container>
Related