16

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.

RulerNature
  • 693
  • 4
  • 13
  • 40
  • have you console `state.loggedIn` inside `mapStateToProps` ? whats it return ? – sayalok Feb 15 '21 at 11:10
  • @sayalok is returning undefined, but `state.auth. loggedIn ` is returning the default state which is `null`. In the login component that is true but in any other component return null – RulerNature Feb 15 '21 at 12:05
  • did you import your action in your main file like in `app.js`? – sayalok Feb 16 '21 at 04:10
  • @sayalok no, not really cause I'm not using them in the app.js – RulerNature Feb 16 '21 at 06:15
  • if you dont import you action how are you gonna find `loggedIn` ? – sayalok Feb 16 '21 at 06:17
  • @sayalok the actions are imported in file where the routes are, if I'm not using actions into the app.js file, why should I import them there ? – RulerNature Feb 16 '21 at 06:24
  • in route console only `state` check if found the `loggedIn` in state object – sayalok Feb 16 '21 at 06:36
  • Hi. Do you mind providing the codesandbox? – noob_nerd Feb 18 '21 at 07:49
  • in mapStateToProps function you got all states that you have in app, so to get ** loggedIn** you have to go through all nested fields. Looks like your login reducer is stored as auth, so you can destruct it like ({ auth: { isLoggedIn }) => – vitaliy kotov Feb 18 '21 at 15:17

6 Answers6

11

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)

n1stre
  • 5,856
  • 4
  • 20
  • 41
  • Using the `render` prop instead of `component` is much worse for performance. Be sure to implement performance optimizations like `memo` if using this method. – Slbox Feb 19 '21 at 20:02
  • @Slbox do you have a source for this? I don't see why there would be a significant difference. – deckele Feb 20 '21 at 01:58
  • @deckele https://medium.com/@migcoder/difference-between-render-and-component-prop-on-react-router-v4-368ca7fedbec – Slbox Feb 20 '21 at 19:41
  • 1
    @Slbox Thanks for the source. However, in the article you provided, the author claims that there is no difference in performance between the `component` or `render` prop of a Route component. – deckele Feb 20 '21 at 23:02
  • I guess I should have read the conclusion. [Here's a Stack answer on the topic.](https://stackoverflow.com/questions/48150567/react-router-difference-between-component-and-render/48152635#48152635) – Slbox Feb 20 '21 at 23:06
  • As well as your answer is a copy mine. Though I made a lot of tweaks to my copy. Anyway, sorry about that, didn't want to be rude or smthng. I'll update my answer soon to give you some credits – n1stre Feb 25 '21 at 07:41
4

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);
SuleymanSah
  • 17,153
  • 5
  • 33
  • 54
  • the thing is I have used your example and not really understand why on browser refresh the route become unknown, not sure why :| – RulerNature Feb 24 '21 at 13:21
  • @RulerNature redux state is lost when browser refreshes, that's normal. You can check redux-persist package. https://github.com/rt2zz/redux-persist – SuleymanSah Feb 24 '21 at 15:24
2

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

unlimit
  • 3,672
  • 2
  • 26
  • 34
jean182
  • 3,213
  • 2
  • 16
  • 27
1

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.

hersheys17
  • 61
  • 3
1

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;
Nishant Singh
  • 477
  • 4
  • 8
1

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>
Ever Dev
  • 1,882
  • 2
  • 14
  • 34