React router private routes / redirect not working

2020-02-16 08:03发布

I have slightly adjusted the React Router example for the private routes to play nice with Redux, but no components are rendered when Linking or Redirecting to other 'pages'. The example can be found here:

https://reacttraining.com/react-router/web/example/auth-workflow

Their PrivateRoute component looks like this:

const PrivateRoute = ({ component: Component, ...rest }) => (
  <Route {...rest} render={props => (
    fakeAuth.isAuthenticated ? (
      <Component {...props}/>
    ) : (
      <Redirect to={{
        pathname: '/login',
        state: { from: props.location }
      }}/>
    )
  )}/>
)

But, because I have incorporated it in a Redux application, I had to adjust the PrivateRoute a little so I can access the redux store, as well as the route Props:

const PrivateRouteComponent = (props) => (
    <Route {...props.routeProps} render={() => (
    props.logged_in ? (
        <div>{props.children}</div>
        ) : (
        <Redirect to={{
            pathname: '/login',
            state: { from: props.location }
        }} /> )
    )} />
);

const mapStateToProps = (state, ownProps) => {
    return {
        logged_in: state.auth.logged_in,
        location: ownProps.path,
        routeProps: {
            exact: ownProps.exact,
            path: ownProps.path
        }
    };
};

const PrivateRoute = connect(mapStateToProps, null)(PrivateRouteComponent);
export default PrivateRoute

Whenever I'm not logged in and hit a PrivateRoute, I'm correctly redirected to /login. However, after for instance logging in and using a <Redirect .../>, or clicking any <Link ...> to a PrivateRoute, the URI updates, but the view doesn't. It stays on the same component.

What am I doing wrong?


Just to complete the picture, in the app's index.js there is some irrelevant stuff, and the routes are set up like this:

ReactDOM.render(
    <Provider store={store}>
        <App>
            <Router>
                <div>
                    <PrivateRoute exact path="/"><Home /></PrivateRoute>
                    // ... other private routes
                    <Route path="/login" component={Login} />
                </div>
            </Router>
        </App>
    </Provider>,
    document.getElementById('root')
);

9条回答
forever°为你锁心
2楼-- · 2020-02-16 08:11

Typescript

If you are looking for a solution for Typescript, then I made it work this way,

const PrivateRoute = ({ component: Component, ...rest }: any) => (
    <Route
        {...rest}
        render={props =>
            localStorage.getItem("authToken") ? (
                <Component {...props} />
            ) : (
                    <Redirect
                        to={{
                            pathname: "/login",
                            state: { from: props.location }
                        }}
                    />
                )
        }
    />
);

<Router>
    <Switch>
        <PrivateRoute exact path="/home" component={Home} />
        <Route path="/login" component={Login} />
    </Switch>
</Router>

Just in case you want to go by creating a class then something like this,

class PrivateRoute extends Route {
    render() {
        if (localStorage.getItem("authToken")) {
            return <Route {...this.props} />
        } else {
            return <Redirect
                to={{
                    pathname: "/login",
                    state: { from: this.props.location }
                }}
            />
        }
    }
}
查看更多
Anthone
3楼-- · 2020-02-16 08:14

Well, I think the answer to this question should really be more detailed, so here I am, after 4 hours of digging.

When you wrap your component with connect(), React Redux implements shouldComponentUpdate (sCU if you search answers on github issues) on it and do shallow comparison on props (it goes throug each key in the props object and check if values are identical with '==='). What it means in practice is that your component is considered Pure. It will now change only when its props change and only then! This is the key message here. Second key message, React router works with context to pass the location, match and history objects from Router to Route component. It doesn't use props.

Now let's see in practice what happen, because even knowing that, I find it still pretty tricky:

  • Case 1:

There is 3 key to your props after connecting: path, component, and auth (given by connect). So, in fact, your wrapped component will NOT re-render at all on route changes because it doesn't care. When route changes, your props don't change and it will not update.

  • Case 3:

Now there is 4 keys to your props after connecting: path, component, auth and anyprop. The trick here is that anyprop is an object that is created each time the component is called. So whenever your component is called this comparison is made: {a:1} === {a:1}, which (you can try) gives you false, so your component now updates every single time. Note however that your component still doesn't care about the route, its children do.

  • Case 2:

Now that's the mystery here, because i guess you call this line in your App file, and there should be no reference to "auth" in there, and you should have an error (at least that's what i am guetting). My guess is that "auth" in your App file references an object defined there.

Now what should we do ?

I see two options here:

  1. Tell React Redux that your component is not pure, this will remove the sCU injection and your component will now correctly update.

    connect(mapStateToProps, null, null, { pure: false })(PrivateRoute)

  2. Use WithRouter(), which will results in injecting the location, match and history object to your component as props. Now, I don't know the internals, but i suspect React router doesn't mutate those objects so each time the route change, its props change (sCU returns true) as well and your component correctly updates. The side effect of this is that your React tree is now polluted with a lot of WithRouter and Route stuff...

Reference to the github issue: Dealing with Update Blocking

You can see here that withRouter is intended as a quickfix but not a recommended solution. using pure:false is not mentionned so I don't know how good this fix could be.

I found a 3rd solution, though it's unclear to me if it is really a better solution than withRouter, using Higher Order Component. You connect your Higher-order Component to the Redux store, and now your route doesn't give a damn about what it renders, the HOC deals with it.

import Notlogged from "./Notlogged";    
function Isloggedhoc({ wrap: Component, islogged, ...props }) {
      return islogged ? <Component {...props} /> : <Notlogged {...props} />;
    }

const mapState = (state, ownprops) => ({
      islogged: state.logged,
      ...ownprops
    });

export default connect(mapState)(Isloggedhoc);

in your App.js

<Route path="/PrivateRoute" render={props => <Isloadedhoc wrap={Mycomponent} />} />

You could even make a curried function to shorten it a bit:

function isLogged(component) {
  return function(props) {
    return <Isloadedhoc wrap={component} {...props} />;
  };
}

Using it like that:

<Route path="/PrivateRoute" render={isLogged(Mycomponent)} />
查看更多
【Aperson】
4楼-- · 2020-02-16 08:17

I have struggled with this issue as well, and here is my solution.

Instead of passing isAuthenticated to every < PrivateRoute> component, you just need to get isAuthenticated from state in < PrivateRoute> itself.

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

// isAuthenticated is passed as prop here
const PrivateRoute = ({component: Component, isAuthenticated , ...rest}) => {
    return <Route
        {...rest}
        render={
            props => {
                return isAuthenticated ?
                    (
                        <Component {...props} />
                    )
                    :
                    (
                        <Redirect
                            to={{
                                pathname: "/login",
                                state: {from: props.location}
                            }}
                        />
                    )
            }
        }
    />
};

const mapStateToProps = state => (
    {
        // isAuthenticated  value is get from here
        isAuthenticated : state.auth.isAuthenticated 
    }
);

export default withRouter(connect(
    mapStateToProps, null, null, {pure: false}
)(PrivateRoute));
查看更多
【Aperson】
5楼-- · 2020-02-16 08:21

You need to wrap your Route with <Switch> tag

ReactDOM.render(
<Provider store={store}>
    <App>
        <Router>
            <div>
                <Switch>
                   <PrivateRoute exact path="/"><Home /></PrivateRoute>
                   // ... other private routes
                   <Route path="/login" component={Login} />
                </Switch>
            </div>
        </Router>
    </App>
</Provider>,
document.getElementById('root'));
查看更多
闹够了就滚
6楼-- · 2020-02-16 08:26

I managed to get this working using the rest parameter to access the data from mapStateToProps:

const PrivateRoute = ({component: Component, ...rest}) => {
  const {isAuthenticated} = rest;

  return (
    <Route {...rest} render={props => (
      isAuthenticated ? (
        <Component {...props}/>
      ) : (
        <Redirect to={{
          pathname: '/login',
          state: {from: props.location}
        }}/>
      )
    )}
    />
  );
};

PrivateRoute.propTypes = {
  isAuthenticated: PropTypes.bool.isRequired,
};

function mapStateToProps(state) {
  return {
    isAuthenticated: state.user.isAuthenticated,
  };
}

export default connect(mapStateToProps)(PrivateRoute);
查看更多
Viruses.
7楼-- · 2020-02-16 08:31

According to react-router documentation you may just wrap your connect function with withRouter:

// before
export default connect(mapStateToProps)(Something)

// after
import { withRouter } from 'react-router-dom'
export default withRouter(connect(mapStateToProps)(Something))

This worked for me and my views started to be updated along with routes in this case.

查看更多
登录 后发表回答