5

Inside my React JS project, I am working on the PrivateRoutes. I have gone through this example of private routing and authenticating using react-router-dom.

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

According to this documentation, they have created a PrivateRoute as a stateless component.

But my requirement is to convert it to stateful React component as I want to connect my PrivateRoute component to redux store.

Here is my code.

stateless component

import React from 'react';
import {Route, Redirect} from 'react-router-dom';
import {auth} from './Authentication';

const PrivateRoute = ({ component: Component, ...rest }) => (
    <Route
      {...rest}
      render={props =>
        auth.isAuthenticated ? (
          <Component {...props} />
        ) : (
          <Component {...props} action="login"/>
        )
      }
    />
  );

  export default PrivateRoute;

I converted this component to stateful React component like this.

stateful React component

import React from 'react';
import {Route, Redirect} from 'react-router-dom';
import {auth} from './Authentication';
import {connect} from 'react-redux';

  class PrivateRoute extends React.Component {
    render({ component: Component, ...rest }) {
      return (
        <Route
          {...rest}
          render={props =>
            this.props.customer.isAuthenticated ? (
              <Component {...props} />
            ) : (
              <Component {...props} action="login"/>
            )
          }
        />
      );
    }
  }
  export default connect(state => state)(PrivateRoute);

Here, I am reading the data from redux store to check whether the user is authenticated or not.

But the way I am converting the stateless component to stateful isn't correct.

Am I passing the arguments render({ component: Component, ...rest }) correctly?

Will connecting the PrivateRoute with redux store create any problem with props as state=>state will map state to props as well as ...rest will have props object?

Not sure what is happening inside the code.

Update AppRouter.js

import React from 'react';
import {BrowserRouter, Route, Switch} from 'react-router-dom';
import {TransitionGroup, CSSTransition} from 'react-transition-group';
import PrivateRoute from './PrivateRoute';

import HomePage from './../components/HomePage';
import AboutUs from './../components/AboutUs';
import ContactUs from './../components/ContactUs';
import PageNotFound from './../components/PageNotFound';
import RestaurantList from '../components/RestaurantList';
import RestaurantMenu from '../components/RestaurantMenu';
import UserDetails from '../components/UserDetails';
import OrderConfirmation from '../components/OrderConfirmation';
import CustomerAccount from '../components/CustomerAccount';
import Logout from '../components/sections/Logout';


export default () => {
    return (
        <BrowserRouter>
            <Route render={({location}) => (
                <TransitionGroup>
                    <CSSTransition key={location.key} timeout={300} classNames="fade">
                        <Switch location={location}>
                            <Route path="/" component={HomePage} exact={true}/>
                            <Route path="/about" component={AboutUs} />
                            <Route path="/contact" component={ContactUs} />
                            <Route path="/restaurants" component={RestaurantList} />
                            <Route path="/select-menu" component={RestaurantMenu} />
                            <PrivateRoute path="/user-details" component={UserDetails} />
                            <PrivateRoute path="/order-confirmation" component={OrderConfirmation} />
                            <PrivateRoute path="/my-account" component={CustomerAccount} />
                            <PrivateRoute path="/logout" component={Logout} />

                            <Route component={PageNotFound} />
                        </Switch>
                    </CSSTransition>
                </TransitionGroup>
            )} />

        </BrowserRouter>
    );
}
Vishal Shetty
  • 1,618
  • 1
  • 27
  • 40
  • 1
    If you are passing all required data to `props` then why do you need it to convert into stateful component though there are no `state`? – Hriday Modi Jun 23 '18 at 10:18
  • I wanted to connect this component to redux store, and I did connect it but `this.props.customer.isAuthenticated` was unavailable to me, so I thought I should convert it to a stateful component. – Vishal Shetty Jun 23 '18 at 10:36

4 Answers4

7

In general, converting a stateless functional component (SFC) to a Component is done like this:

  1. Create the class shell for it.

  2. Copy the SFC's body to the render method. If the SFC was an arrow function, add a return as necessary to render.

  3. Change any references to props in the render method to this.props (or just add const { props } = this; at the top). SFCs receive their props in their arguments, but a component receives them as arguments to its constructor; the default constructor will save them as this.props.

    In your case, it's using destructuring on its arguments, so you could do the same with this.props on the right-hand side of the destructuring:

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

That's it. In your code, you've added parameters to the render function, but it doesn't get called with any arguments, and you've only changed props to this.props a bit haphazardly (including changing auth.isAuthenticated to this.props.customer.isAuthenticated for some reason).

So applying 1-3 above:

// #1 - the shell
class PrivateRoute extends React.Component {
  // #2 - `render`, with the body of the SFC inside
  render() {
    // #3 - destructure `this.props`
    const { component: Component, ...rest } = this.props;
    // #2 (part 2) - add `return`
    return <Route
      {...rest}
      render={props =>
        auth.isAuthenticated ? (
          <Component {...props} />
        ) : (
          <Component {...props} action="login"/>
        )
      }
    />;
  }
}
Community
  • 1
  • 1
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • please address the following part too: "Will connecting the `PrivateRoute` with redux store create any problem with `props` as `state=>state` will map `state` to `props` as well as `...rest` will have `props` object?". what if the component gets a prop `user` from where its used and there is a state field `user` too, which value of user will i get in render function? – Inus Saha Jun 23 '18 at 08:01
  • @InusSaha - I wouldn't know, I don't use Redux. – T.J. Crowder Jun 23 '18 at 08:06
  • @InusSaha @Vishal Shetty Yes, connecting `PrivateRoute` with redux store will create problem in this case because external props passed to component will not be available in component props. You will have to handle it in `mapStateToProps` function – Hriday Modi Jun 23 '18 at 10:24
1

Your stateful component should be:

class PrivateRoute extends React.Component {
  render() {
    const { component: Component, ...rest } = this.props;
    return (
      <Route
        {...rest}
        render={props =>
          this.props.customer.isAuthenticated ? (
            <Component {...props} />
          ) : (
            <Component {...props} action="login"/>
          )
        }
      />
    );
  }
}

Please see that there is some issue in render parameter of Route. Here you have props as function param but still using this.props.customer, don't know the use case hence please fix it as per your application.

Apart from it Component and all the other data is already there in props of the component. It won't be available in parameter of render method in component. Same destructuring as available in stateless component can be written in render method as shown in code above.

Will connecting the PrivateRoute with redux store create any problem with props?

Yes, it would. The way you have connected to the store will make store data available in props of component but external props passed to component will not be available.

For that you have to handle it in mapStateToProps function:

const mapStateToProps = (state, ownProps) => ({
    ...state,
    ...ownProps
});

Here mapStateToProps has second parameter which has the external own props passed to component. So you have to return it as well to make it available in component props.

Now connect would be like:

export default connect(mapStateToProps)(PrivateRoute);
Hriday Modi
  • 2,001
  • 15
  • 22
0

I was having two queries.

1) How to convert to Stateful Functional Component? 2) After connecting to the redux store will the props create a problem?

My first query was solved by the answer provided by T.J.Crowder.

For a second query, I tried connecting the redux store to the PrivateRoute and I did get the data I was looking for.

Here is the code which worked for me.

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

class PrivateRoute extends React.Component {
  render() {
    const { component: Component, ...rest } = this.props;
    const {customer} = this.props;

    return <Route
      {...rest}
      render={props =>
        customer.isAuthenticated ? (
          <Component {...props} />
        ) : (
          <Component {...props} action="login"/>
        )
      }
    />;
  }
}

export default connect(state => state)(PrivateRoute);

Using this code I got the data that is coming from the routes, as well as the redux state inside the props.

This is getting data coming from the routes const { component: Component, ...rest } = this.props;

This is the data coming from the redux store. const {customer} = this.props;

Vishal Shetty
  • 1,618
  • 1
  • 27
  • 40
  • this is fine if state and props properties are different. but consider if you have same name property in state and props both then there may be problem. you may loose one property. – Inus Saha Jun 23 '18 at 10:48
  • yeah, you are right, needs to find out the solution to this problem as well. – Vishal Shetty Jun 23 '18 at 10:50
  • @VishalShetty I don't think routes data will be available in props using this code OR you have routes data in store? – Hriday Modi Jun 23 '18 at 10:55
  • @HridayModi sorry for that, I mean the props passed to the route will be available inside the props as well as redux store data will be available. As `Inus Saha` said if props coming from the route and the redux store has same property name then we will be loosing any one property. – Vishal Shetty Jun 23 '18 at 10:59
  • @HridayModi connect passes the complete redux store/state to a component not just any child state. so its better you explicitly choose which data you need from state. read my answer. – Inus Saha Jun 23 '18 at 11:00
  • @VishalShetty I got that but what i am saying is "props passed to the route will not be available inside the props of component". Because connect will pass only store data in props. – Hriday Modi Jun 23 '18 at 11:02
  • @HridayModi no this not true, I am getting both the data correctly inside the props. – Vishal Shetty Jun 23 '18 at 11:03
  • @VishalShetty That is strange. – Hriday Modi Jun 23 '18 at 11:05
  • @HridayModi check the updated question, I have added my `AppRouter.js` file. The `` is the protected route, so `path` and `component` both of the props I am getting inside the `PrivateRoute`, as well as I am getting redux store data. – Vishal Shetty Jun 23 '18 at 11:10
  • @VishalShetty Yes checked the code but still amazed that how it's working – Hriday Modi Jun 23 '18 at 11:33
0

@T.J.Crowder has already written how to convert stateless component to stateful component in those 3 steps. so i will just write about connecting component to redux store like you did.

I think connected components should always define mapStateToProps and explicitly declare which data they depend on from the state.

because the connected component rerenders if the connected property changes. so it would be a bad idea to connect the whole application state to a component. as it would mean that wheneever anything changes in application state rerender all connected components.

better we define explicitly like the following that we depend on a property called data (or anything you have) from the state. so in this case this component will only rerender if state.data changes it wont rerender if state.xyz changes.

and this way you can take state.data and name it as you wish so it would not conflict with any existing props of the component.

const mapStateToProps = (state, ownProps) => ({
    data: state.data
});

export default connect(mapStateToProps)(PrivateRoute);
Inus Saha
  • 1,918
  • 11
  • 17
  • I thought once we connect the component to redux store, then the component gets render even if the store data updated isn't relative to the component. – Vishal Shetty Jun 23 '18 at 11:02
  • no, its not like that. you can read more here: https://stackoverflow.com/a/40386189/4541018 read the first paragraph and read more about connect and component rerender. – Inus Saha Jun 23 '18 at 11:08