16

I have this PrivateRoute component (from the docs):

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

I would like to change isAuthenticated to an aysnc request isAuthenticated(). However, before the response has returned the page redirects.

To clarify, the isAuthenticated function is already set up.

How can I wait for the async call to complete before deciding what to display?

tommyd456
  • 9,203
  • 21
  • 74
  • 144

3 Answers3

17

If you aren't using Redux or any other kind of state management pattern you can use the Redirect component and components state to determine if the page should render. This would include setting state to a loading state, making the async call, after the request has completed save the user, or lack of user to state and render the Redirect component, if criteria is not met the component will redirect.

class PrivateRoute extends React.Component {
  state = {
    loading: true,
    isAuthenticated: false,
  }
  componentDidMount() {
    asyncCall().then((isAuthenticated) => {
      this.setState({
        loading: false,
        isAuthenticated,
      });
    });
  }
  render() {
    const { component: Component, ...rest } = this.props;
    if (this.state.loading) {
      return <div>LOADING</div>;
    } else {
      return (
        <Route {...rest} render={props => (
          <div>
            {!this.state.isAuthenticated && <Redirect to={{ pathname: '/login', state: { from: this.props.location } }} />}
            <Component {...this.props} />
          </div>
          )}
        />
      )
    }
  }
}
pizzarob
  • 10,072
  • 4
  • 37
  • 63
  • `state = …` didn't work for me inside the class declaration. I had to use the constructor to set the initial state instead. – raddevon Oct 04 '17 at 15:53
  • 1
    You would need to configure babel to use class properties transform, or use babel preset stage 0 - 2 for that to work @raddevon – pizzarob Oct 04 '17 at 19:11
  • @pizza-r0b can you tell me how the index.js router setup would like for your code? – AnBisw Jun 08 '18 at 21:25
  • how secure is that really holding on to isAuthenticated as a state variable? Couldn't the user just open the dev console and change the value to true? – J. Pedro Sep 28 '18 at 14:20
  • 2
    @J.Pedro by nature client side JS is insecure. To change the isAuthenticated variable to true would require a little more than just opening the dev console - you'd probably have to sift through compiled, minified code and then proxy a new JS file. If someone was determined enough to do that they would be able to see some `isAuthenticated` UI, however the backend is what is responsible for displaying all authenticated data and verifying the user is authenticated and allowed to see the data. So even if someone saw authenticated UI - it's up to the backend to secure sensitive data. – pizzarob Sep 29 '18 at 16:18
  • 1
    @pizza-r0b componentDidMount() method calls just once. It works only calls when user click on first link. It does not satisfies the condition i.e. whenever user visits to private route, componentDidMount or the method containing promise should call. How can I achieve it? – Rohit Sawai Dec 03 '19 at 05:26
  • Why not just authenticate one time and save the authentication token? – pizzarob Dec 04 '19 at 14:33
10

In case anyone's interested in @CraigMyles implementation using hooks instead of class component:

export const PrivateRoute = (props) => {
    const [loading, setLoading] = useState(true);
    const [isAuthenticated, setIsAuthenticated] = useState(false);

    const { component: Component, ...rest } = props;

    useEffect(() => {
        const fetchData = async () => {
            const result = await asynCall();

            setIsAuthenticated(result);
            setLoading(false);
        };
        fetchData();
    }, []);

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


Which works great when called with:

<PrivateRoute path="/routeA" component={ComponentA} />
<PrivateRoute path="/routeB" component={ComponentB} />
  • 2
    I believe you can prevent unnecessary asynchronous calls by passing `[setIsAuthenticated]` as the second argument in `useEffect()`. – iamfrank Jul 26 '20 at 19:08
  • Why is it necessary to have a state that keeps track of loading? Would it be possible to revise the ternary statement to only take into account the authentication state variable? – iamfrank Jul 26 '20 at 19:09
  • Totally forgot the second argument, it's a must-have! For the necessity of having a separate "loading", since the "isAuthenticated", I suppose you can start that last one undefined and go from there, it's a valid approach! Thanks for the tips – Josnei Luis Olszewski Junior Aug 10 '20 at 01:17
  • This method just renders a loading screen but it does not update after the async calls ends, it just stays in the loading screen, what do I do about that? – sid_508 Jan 29 '21 at 13:59
5

@pizza-r0b's solution worked perfectly for me. However, I had to amend the solution slightly to prevent the loading div from being displayed multiple times (once for every PrivateRoute defined within the app) by rendering the loading div inside - instead of outside - the Route (similar to React Router's auth example):

class PrivateRoute extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      loading: true,
      isAuthenticated: false
    }
  }

  componentDidMount() {
    asyncCall().then((isAuthenticated) => {
      this.setState({
        loading: false,
        isAuthenticated
      })
    })
  }

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

An extract from my App.js for completeness:

<DashboardLayout>
  <PrivateRoute exact path="/status" component={Status} />
  <PrivateRoute exact path="/account" component={Account} />
</DashboardLayout>
Craig Myles
  • 3,739
  • 3
  • 31
  • 34
  • Hey Craig, this actually works but can you tell me where `/login` will be routed to? I mean to which component? It should be a "Login" component somewhere but If I define `/login` like `` in App.js it would be an infinite loop. – AnBisw Jun 08 '18 at 20:13
  • 1
    @Annjawn You wouldn't define the `/login` route as a `PrivateRoute`, but a normal `Route`. Login pages are not private. – empz Jul 16 '18 at 14:29