2

I am trying to make an authenticated route in React Router v4 as per this example. Showing the code for posterity:

function PrivateRoute ({component: Component, authed, ...rest}) {
  return (
    <Route
      {...rest}
      render={(props) => (!!authed)
        ? <Component {...props} />
        : <Redirect to={{pathname: '/login', state: {from: props.location}}} />}
    />
  )
}

My authentication state (authed), which is initialized as an empty object at the reducer, is derived from a Redux store. This is how my App.js looks like:

class App extends Component {
  componentDidMount() {
    const token = localStorage.getItem("token");
    if (token) {
      this.props.fetchUser();
    }
  }

  render() {
    return (
      <Router>
        <div>
          <PrivateRoute authed={this.props.authed} path='/dashboard' component={Dashboard} />
          />
        </div>
      </Router>
    );
  }
}

The problem is that the authed state starts as undefined and then, once the Router component is mounted, it updates the state to true. This is however a bit late, because the user would be already redirected back to the login page. I also tried to replace the componentDidMount() lifecycle method, with the componentWillMount() but that did not fix the problem either.

What strategies would you suggest?

UPDATE 1: The only way I get around this is by testing for the authed state before returning the <Route /> component such as this:

  render() {
    if (!!this.props.authed) {
      return (
        <Router>
      <div>
      ...

UPDATE 2: I am using Redux Thunk middleware to dispatch the action. The state is being passed as props correctly - I am using console.log() methods inside the PrivateRoute component to verify that the state mutates correctly. The problem is of course that it is mutating late, and the Route is already redirecting the user.

Pasting code of reducer and action...

Action:

export const fetchUser = () => async dispatch => {
  dispatch({ type: FETCHING_USER });
  try {
    const res = await axios.get(`${API_URL}/api/current_user`, {
      headers: { authorization: localStorage.getItem("token") }
    });
    dispatch({ type: FETCH_USER, payload: res.data });
  } catch (err) {
    // dispatch error action types
  }
};

Reducer:

const initialState = {
  authed: {},
  isFetching: false
};
...
    case FETCH_USER: // user authenticated
      return { ...state, isFetching: false, authed: action.payload };
James
  • 2,837
  • 6
  • 31
  • 67
  • How are you getting the values for 'authed', have you tried to investigate why authed call takes long, can you share that code. – alowsarwar Apr 11 '18 at 14:33
  • @alowsarwar it's an Axios call to a nodejs server that returns a Passport user object (deserialized from jwt token) – James Apr 11 '18 at 14:36
  • @James Can you tell me about the UPDATE 1 solution, where are you checking `(!!this.props.authed)`? I am having the same problem. – Arnab Mar 03 '19 at 13:43
  • @Arnab I am testing that the *authed* state exists, before rendering the `` components. In other words, to answer your question, I am checking it in my routes file. – James Mar 04 '19 at 07:03
  • @James Okay, thanks. I will try your solution in the meantime I tried to solve it with a different approach. It is not the most preferred method. But now the problem is not happening. You can check my answer, I have posted it below. – Arnab Mar 04 '19 at 13:13

4 Answers4

2

I had the same problem and from my understanding your update #1 should be the answer. However upon further investigation I believe this is an architectural problem. The current implementation of your Private route is dependent on the information being synchronous.

If we think about it pragmatically the ProtectedRoute essentially returns either a redirect or the component based on the state of our application. Instead of wrapping each Route with a component we can instead wrap all the routes in a component and extract our information from the store.

Yes it is more code to write per protected route, and you'll need to test if this is a viable solution.

Edit: Forgot to mention another big reason this is an architectural problem is if the user refreshes a page which is protected they will be redirected.

UPDATE Better solution: On refresh if they are authenticated it will redirect to their target uri https://tylermcginnis.com/react-router-protected-routes-authentication/

Solution 1

//You can make this a method instead, that way we don't need to pass authed as an argument
function Protected(authed, Component, props) {
  return !!authed
    ? <Component {...props} />
    : <Redirect to='/login' />
}

class AppRouter extends React.PureComponent {
  componentDidMount() {
    const token = localStorage.getItem("token");
    if (token) {
      this.props.fetchUser();
    }
  }

  render() {
    let authed = this.props.authed

    return (
      <Router>
          <Route path='/protected' render={(props) => Protected(authed, Component, props)} />
      </Router>
    )
  }
}

class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <AppRouter />
      </Provider>
    )
  }
}

Solution 2 Or we can just check for each component (yes it's a pain)

class Component extends React.Component {
  render() {
    return (
      !!this.props.authed
        ? <div>...</div>
        : <Redirect to='/' />
    )
  }
}
1

The same problem was happening with me, I am using a temporary hack to solve it by storing an encrypted value inside localStorage and then decrypting it in my PrivateRoute component and checking if the value matches.

action.js

localStorage.setItem('isAuthenticated', encryptedText);

PrivateRoute.js

if (localStorage.getItem('isAuthenticated')) {
const isAuth = decryptedText === my_value;
return (
     <Route
        {...rest}
        render={(props) =>
           isAuth ? <Component {...props} /> : <Redirect to="/login" />
        }
     />
  );
} else {
      return <Redirect to="/login" />;
   }

Since localStorage is faster, so it is not unnecessarily redirecting. If someone deletes localStorage they will simply be redirected to /login Note: It is a temporary solution.

Arnab
  • 398
  • 6
  • 12
0

if your App component is connected to the redux store, you probably mean this.props.authed instead of this.state.authed

My authentication state (authed), which is initialized as an empty object at the reducer, is derived from a Redux store

So you are comparing empty object with true here: (props) => authed === true? Why don't you initialize it with a false?

And are you sure the action this.props.fetchUser is switching the state to true?

Maybe you better also post your action and reducer file

Sander Garretsen
  • 1,522
  • 8
  • 18
0

Theoretically, you need to get a promise from the NODE API call which you are not getting right now. You need to make architectural changes. I suggest you use redux-promise-middleware this is a redux middleware. I have a sample project in my github account. Where you will get notified if your call to this.props.fetchUser() is completed or not, based on that using Promise you can handle this async problem you have. Go to that repo if need help ask me.

alowsarwar
  • 733
  • 7
  • 22