How to use React HOC for authenticated routing? - reactjs

I'm trying to add private routing to my app, but I get this error:
"Invariant failed: You should not use <Route> outside a <Router>"
Here is my code:
App.js
class App extends Component {
render() {
return (
<BrowserRouter>
<Route render={({ history }) => (
<div className="App">
<Navbar history={history} />
<Switch>
<Auth path="/" component={HomePage} currUser={this.props.currUser} />
<Route path="/login" render={(props) => (<LoginPage {...props} login={this.props.login} />)} />
</Switch>
</div>
)} />
</BrowserRouter>
);
}
}
const Auth = ({ component: Component, ...rest }) => {
const {currUser} = rest;
return (
<Route {...rest} render=
{props =>
currUser ?
(<Component {...props} currUser={currUser.name} />) :
(<Redirect to={{ pathname: "/login", state: currUser }} />)
} />
)
}
const mapStateToProps = (state) => {
return {
currUser: state.auth.currUser
}
}
const mapDispatchToProps = {
...authActions,
}
export default connect(mapStateToProps, mapDispatchToProps)(Auth);
What am i doing wrong?
And how do I pass props from Redux state to the components in this method?
Thanks!

You wrapped the Auth component with the App component, so your file should export the App. When you export only the Auth, the <Route> tag in Auth is outside the router tag.
export default connect(mapStateToProps, mapDispatchToProps)(App);

Related

React Testing Library Invariant failed: You should not use <Route> outside a <Router>

I'm testing if my components render with Redux successfully with React Testing Library. I'm having trouble having my utility component to pass the renderWithRedux test. This is my App component.
function App() {
return (
<>
<Router>
<NavBar />
<div className="container">
<Switch>
<Route exact path='/' component={Home}/>
<AuthRoute exact path='/login' component={Login} />
<AuthRoute exact path='/signup' component={Signup} />
<Route exact path='/users/:handle' component={UserProfile} />
<Route exact path='/users/:handle/post/:postId' component={UserProfile} />
</Switch>
</div>
</Router>
</>
);
}
Here is my AuthRoute utility component.
const AuthRoute = ({ component: Component, authenticated, ...rest }) => (
// if authenticated, redirect to homepage, otherwise redirect to signup or login
<Route
{...rest}
render={(props) =>
authenticated === true ? <Redirect to='/' /> : <Component {...props} />
}
/>
);
AuthRoute.test.js
const renderWithRedux = () => render(
<Provider store={myStore}>
<AuthRoute />
</Provider>
);
it('renders with Redux', () => {
const {} = renderWithRedux(<AuthRoute />);
});
I've attempted the solutions from Invariant failed: You should not use <Route> outside a <Router>, but to no avail. I appreciate any help, thank you.
Render the component under test into a router
import { MemoryRouter } from 'react-router-dom';
const renderWithRedux = ({ children }) => render(
<Provider store={myStore}>
{children}
</Provider>
);
it('renders with Redux', () => {
const {} = renderWithRedux(
<MemoryRouter>
<AuthRoute />
</MemoryRouter>
);
});
Just like the Provider to wrap redux things you have to wrap your components with routes using MemoryRouter for the tests.
import { MemoryRouter } from 'react-router';
Basically, you have two wrapper elements. It should go something like this, for example, renderWithReduxWrapp => renderWithRouter => YourTestingComponent.
I had a similar issue when trying to test Button render (which has a Link) depending on props, and was able to solve it by creating some helper functions.
Here is the example:
This is the main component, UserCard.js, which renders user data from redux, and only shows a button if withButton props is passed.
import React from "react";
import { Link } from "react-router-dom";
import { Button } from "react-bootstrap";
const CardComponent = ({ withButton }) => {
const userInfo = useSelector((state) => getUserSelector(state));
return (
<div>
<div>{userInfo}</div>
{withButton && (
<Link to="/settings" className="button-link">
<Button block>EDIT CONTACT INFO</Button>
</Link>
)}
</div>
);
};
export default CardComponent;
This is a CardComponent.test.js file.
First, you need to add these lines of code
const ReduxWrapper = ({ children }) => {
<Provider store={store}>{children} </Provider>;
}
const AppWrapper = ({ children }) => (
<BrowserRouter>
<ReduxWrapper>{children}</ReduxWrapper>
</BrowserRouter>
);
const renderWithRouter = (ui, { route = '/' } = {}) => {
window.history.pushState({}, 'Test page', route);
return render(ui, { wrapper: AppWrapper });
};
After that, you need to start your test with renderWithRouter instead of just render method.
it('should render settings button if prop withButton is passed', () => {
renderWithRouter(<CardComponent withButton />, { wrapper: ReduxWrapper });
// apply you code here. I only needed to check if the button is renederd or not.
const settingsButton = screen.queryByText(/edit contact info/i);
expect(settingsButton).toBeInTheDocument();
});

Access isAuthenticated prop using Redux

I think this is a relatively easy problem, however, I'm getting a little confused using Redux. Basically I'm trying to create a PrivateRoute Component and I can't figure out how to access the isAuthenticated prop. Help is appreciated.
UPDATE
Path: App.js
class App extends Component {
componentDidMount() {
store.dispatch(loadUser());
}
render() {
return (
<Provider store={store}>
<Router>
<Header />
<Switch>
<Route exact path="/login">
<ErrorBoundary>
<LoginPage />
</ErrorBoundary>
</Route>
<PrivateRoute path="/purchase_order">
<p>Test</p>
</PrivateRoute>
</Switch>
</Router>
</Provider>
);
}
}
Path: PrivateRoute
const PrivateRoute = ({ children, ...rest }) => (
<Route
{...rest}
render={({ location }) =>
this.props.isAuthenticated ? (
children
) : (
<Redirect
to={{
pathname: '/login',
state: {
from: location
}
}}
/>
)
}
/>
);
const mapStateToProps = (state) => ({
auth: state.auth
});
export default connect(mapStateToProps)(PrivateRoute);
You have done great, you also have structured the functional component very well for PrivateRoute, but there is a mistake while accessing the props, and using as this.props.isAuthenticated, first thing this.props is used only and only in class-based React component or you can use it as (props.isAuthenticated assumed that you have passed isAutheticated prop in PrivateRoute React functional component).
Points you have done-
You have connected your global state to this PrivateRoute functional component.
You have structurally defined your PrivateRoute functional component.
Redux state -
state: {
auth: {userId: 'ob2-51-hw-s124' // any random string based on your API},
otherReducer: {...}
}
Points to be done-
import connect from the react-redux module.
import Route, Redirect from the react-router-dom module.
Final Code
AppRouter.js
// module import
import LandingPage from '...';
import NotFoundPage from '...';
import DashBoardPage from '...';
import LoginPage from '...';
// React functional component that can be imported in App.js
const AppRouter = () => (
<Router history={history}>
<div>
<Switch>
<PublicRoute path="/" exact={true}>
<LandingPage />
</PublicRoute>
<PrivateRoute path="/dashboard">
<DashboardPage />
</PrivateRoute>
<PrivateRoute path="/login">
<LoginPage />
</PrivateRoute>
<PublicRoute path="/help" >
<HelpPage />
</PublicRoute>
<Route>
<NotFoundPage />
</Route>
</Switch>
</div>
</Router>
);
export default AppRouter;
PrivateRoute.js
// module import
import React form 'react';
import { connect } from 'react-redux';
import { Route, Redirect } from 'react-router-dom';
// React functional PrivateRoute component
export const PrivateRoute = ({
auth,
children,
...rest
}) => {
const isAuthenticated = !!auth.userId; // converting auth.userId(typeof string) to false/true(typeof boolean)
return (
<Route {...rest} component={(props) => (
isAuthenticated ? (
children
) : (
<Redirect
to={{
pathname: '/login',
state: {
from: props.location
}
}}
/>
)
)}
/>
);
}
// redux connect
const mapStateToProps = (state) => ({
auth: state.auth
});
export default connect(mapStateToProps)(PrivateRoute);
Your mapStateToProps function shows that you put the state.auth value into the auth prop
You are using a function component, so there is no this.
So, assuming that isAuthenticated is a property of auth, then:
rest.auth.isAuthenticated otherwise: rest.auth
You can also destructure the auth from the props and get direct access to it. Since you don't need to pass it into the route.
const PrivateRoute = ({ children, auth, ...rest }) => (
<Route
{...rest}
render={({ location }) =>
auth.isAuthenticated ? (
children
) : (
<Redirect
to={{
pathname: '/login',
state: {
from: location
}
}}
/>
)
}
/>
);
const mapStateToProps = (state) => ({
auth: state.auth
});
export default connect(mapStateToProps)(PrivateRoute);

Routes requiring authentication in react another way to handle (PrivateRoutes)

I'm looking for a way to do some route protection with react-router-4. Looking at an example in the documentation, they create a Component which is rendered like this:
<PrivateRoute path="/protected" component={Protected} />
and the privateRoute component:
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
fakeAuth.isAuthenticated ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/login',
state: { from: props.location }
}}/>
)
)}/>
)
I don't really understand why I should want to pass the "Protected" component as a property and then have to take care of spreading all the ...props and ...rest.
Before I was reading this doc (and other example), I created the following code, which just nest the routes in another component which takes care of the authentication part.
Because my example (which seems to work perfectly well), looks way more simplistic, I must be missing something.
Are there any downsides on this approach?
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
import Nav from './Nav';
// dummy
const Auth = {
isAuthenticated: () => { return true; }
}
const Home = () => <h1>Home</h1>
const SignIn = () => <h1>SignIn</h1>
const About = () => <h1>About</h1>
class PrivateOne extends Component {
render() {
console.log(this.props);
return <h1>Private</h1>
}
}
const PrivateTwo = () => <h1>PrivateTwo</h1>
const PrivateThree = () => <h1>PrivateThree</h1>
const NotFound = () => <h1>404</h1>
const Private = ({isAuthenticated, children}) => {
return(
isAuthenticated ? (
<div>
<h1>Private</h1>
{children}
</div>
) : (
<Redirect to={{
pathname: '/sign_in',
}}/>
)
)
}
const App = () =>
<div>
<Router>
<div>
<Nav />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/sign_in" component={SignIn} />
<Private isAuthenticated={Auth.isAuthenticated()}> {/* or some state later on */}
<Route path="/private1" component={PrivateOne} />
<Route path="/private2" component={PrivateTwo} />
<Route path="/private3" component={PrivateThree} />
</Private>
<Route component={NotFound} />
</Switch>
</div>
</Router>
</div>
export default App;

React route authentication

I am trying to create a server-side auth guard for react route. My flow is like... Whenever a route is hit on the front-end, a backend call is made to check if the user's session is present in the Database (since we store session in the database to keep track of sessions).
Here is my app component code:
export default function App() {
const routeComponents = routes.map(({ path, component }) => <AuthenticatedRoute exact path={path} component={component} props={'exact'} key={path} />);
return (
<div>
{window.location.origin === constant.PRODUCTION_DOMAIN_NAME && <Route
path="/"
render={({ location }) => {
if (typeof window.ga === 'function') {
window.ga('set', 'page', location.pathname + location.search);
window.ga('send', 'pageview');
}
return null;
}}
/>}
<Switch>
<Route path="/login" component={LoginPage} />
{routeComponents}
</Switch>
<ToastContainer autoClose={constant.TOASTER_FACTOR} closeButton={false} hideProgressBar />
</div>
);
}
App.propTypes = {
component: PropTypes.any,
};
I have segregated the authenticated route into a separate class component
like:
export class AuthenticatedRoute extends React.Component {
componentWillMount() {
//I call backend here to check if user is authenticated by session Id in DB
}
render() {
const { component: Component, ...rest } = this.props;
return (
<Route exact {...rest} render={(props) => this.state.isAuthenticated ? <Component {...props} /> : <Redirect to="/login" />} />
);
}
}
AuthenticatedRoute.propTypes = {
component: PropTypes.any,
....
};
const mapStateToProps = (state) => ({
reducer: state.get('myReducer'),
});
export default connect(mapStateToProps, { func })(AuthenticatedRoute);
But I face an issue where the login page is redirected twice. Could someone let me know a better approach for this?

React router v4 with redux protected routes and auth

I'm using react router v4 with redux and i want to make use of private and protected route to redirect if user is not logged in.
i have this Routes component:
class Routes extends Component {
render() {
const { auth } = this.props;
// so checks against isAuthenticated can pass
if (auth.isAuthenticated !== undefined) {
return (
<div>
<Route exact component={() => <Home />} />
<PublicRoute
authed={() => auth.isAuthenticated}
path="/login"
component={(routerProps) => <Login {...routerProps} />}
/>
<PublicRoute
authed={() => auth.isAuthenticated}
path="/register"
component={(routerProps) => <Register {...routerProps} />}
/>
<PrivateRoute
authed={() => auth.isAuthenticated}
path="/dashboard"
component={Dashboard}
/>
</div>
);
} else {
return <div></div>
}
}
}
function mapStateToProps(state) {
return {
auth: state.auth
}
}
export default withRouter(connect(mapStateToProps)(Routes));
it is implemented like this:
class Main extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
store.dispatch(checkAuth());
}
render() {
return (
<Provider store={store}>
<Router>
<Theme>
<Routes />
</Theme>
</Router>
</Provider>
);
}
}
This is the PrivateRoute:
export function PrivateRoute({ component: Component, authed, ...rest }) {
const isAuthenticated = authed();
return (
<Route
{...rest}
render={props =>
isAuthenticated === true ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
);
}
What is the best way to pass that auth prop, is it ok if i connect the Routes component and pass the auth from there, or should i be passed from somewhere else, maybe connect each component that needs it and read from there? Also how would this approach play with nested routes, like /dashboard/settings? Thanks
Actually, it is ok to use this type of private route in react, but you should check two moments:
I should check, that you do not have exact attribute, so all your routes like /dashboard/panel1, /dashboard/panel2 will be private to
auth.isAuthenticated}
path="/dashboard"
component={Dashboard}
/>
You will have some problem with connect. There is a simple fix for that:
export default connect(mapStateToProps, null, null, {
pure: false,
})(PrivateRoute);
more information here:
React router private routes / redirect not working

Resources