Send request to check role before render specific component - reactjs

I have a project structure like that:
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import {store, persistor} from './helpers';
import { Main } from './main';
import { PersistGate } from 'redux-persist/integration/react';
import { InitIDB } from './helpers/InitIDB';
require('./bootstrap');
InitIDB();
render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Main />
</PersistGate>
</Provider>,
document.getElementById('root')
);
This is entry point, and then the Main component:
import React from 'react';
import { Route, Switch, Router } from 'react-router-dom';
import { connect } from 'react-redux';
import { history } from '../helpers';
import {PrivateRoute} from "../components";
import { ProjectPage } from '../forms/project';
import { ProfilePage } from '../forms/profile';
import { Login } from '../forms/login';
class Main extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<React.Fragment>
<Router history={history}>
<Switch>
<Route exact path="/login" component={Login} />
<PrivateRoute exact path="/com/projects" component={ProjectPage} />
<PrivateRoute exact path="/com/profiles" component={ProfilePage} />
</Switch>
</Router>
</React.Fragment>
);
}
}
function mapState(state) {
const { alert } = state;
return { alert };
}
const actionCreators = {
clearAlerts: function() {}
}
const connectedApp = connect(mapState, actionCreators)(Main);
export { connectedApp as Main };
//PrivateRoute
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import {LayoutX} from '../forms/layout';
export const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
sessionStorage.getItem('user')
? <LayoutX><Component {...props} /></LayoutX>
: <Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)} />
)
I want when user access private route, before that the program will send a request to server and it will return a response decide user can be access this page or not. I try to send a request with axios in PrivateRoute but it is async request and I can not get response before render. What should I do?

Your PrivateRoute should block render when waiting for the authentication status from API is returned.
You can take a look at a simple implementation of PrivateRoute I wrote in this codesandbox.

Related

Redux Authentication not using updated props?

privateRoute.js
import React, { Component } from "react";
import { BrowserRouter, Route, Switch, Redirect, withRouter} from "react-router-dom";
import { connect } from "react-redux";
function AuthRoute({ component: Component, isAuth, ...rest }) {
console.log(isAuth);
let test = false;
return (
<Route
{...rest}
render={props =>
isAuth.isAuthenticated === true ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
);
}
const mapStateToProps = state => {
{
console.log(state);
return { isAuth: state.auth.isAuthenticated };
}
};
export default withRouter(connect(mapStateToProps, { pure: false })(AuthRoute));
App.js
import React, { Component } from "react";
import { BrowserRouter, Route, Switch, Redirect } from "react-router-dom";
import { library } from "#fortawesome/fontawesome-svg-core";
import PropTypes from "prop-types";
import {
faHome,
faClock,
faTasks,
faStickyNote,
faCalendarWeek
} from "#fortawesome/free-solid-svg-icons";
import { connect } from "react-redux";
import { loadUser, checkAuth } from "./actions/authActions";
import store from "./store";
import Home from "./Home";
import SideNav from "./Components/SideNav";
import Recent from "./Components/Recent";
import TopBar from "./Components/TopBar";
import AddNote from "./AddNote";
import LogIn from "./Components/LogIn/LogIn.js";
import Register from "./Components/Register/Register";
import ToDo from "./Components/ToDo/ToDo";
import AuthRoute from "./privateRoute";
library.add(faHome, faClock, faTasks, faStickyNote, faCalendarWeek);
class App extends Component {
constructor(props) {
super(props);
this.state = {
isAuthenticated: false
};
}
static propTypes = {
isAuthenticated: PropTypes.bool
};
componentDidMount() {
store.dispatch(loadUser());
// this.props.isAuthenticated();
}
LogInContainer = () => {
return <Route path="/login" component={LogIn} />;
};
RegisterContainer = () => {
return <Route path="/register" component={Register} />;
};
DefaultContainer = () => {
return (
<div className="app_container">
<SideNav />
<TopBar />
<Route exact path="/" component={Home} />
<Route path="/recent" component={Recent} />
<Route path="/AddNote" component={AddNote} />
<Route path="/ToDo" component={ToDo} />
</div>
);
};
render() {
return (
<div>
<h1> {this.props.auth.isAuthenticated.toString()}</h1>
<BrowserRouter>
<Switch>
<Route exact path="/login" component={this.LogInContainer} />
<Route exact path="/register" component={this.RegisterContainer} />
<AuthRoute component={this.DefaultContainer} />
</Switch>
</BrowserRouter>
</div>
);
}
}
const mapStateToProps = state => {
return { auth: state.auth };
};
export default connect(mapStateToProps)(App);
When the user logins, an action will be dispatched to authentication the user. In redux dev tools, I can see the user authenticated.
Within privateRoute.js, mapStateToProps is always matching the initial state and not the updated state so isAuth never equates to true and I can't show the protected route?
I've been stuck on this for days, I feel like I'm implementing this wrong however looking at other examples I can't see what I am doing wrong.
The issue is how you are mapping state to props. It looks like your store state is structured as { auth: { isAuthenticated: boolean } }. However in the private route map state to props, you are trying to access effectively auth.isAuthenticated.isAuthenticated which will always be false/undefined. You need to go level higher in your state structure when mapping state to props. Try updating the private route component to:
import React, { Component } from "react";
import { BrowserRouter, Route, Switch, Redirect, withRouter } from "react-router-dom";
import { connect } from "react-redux";
function AuthRoute({ component: Component, isAuth, ...rest }) {
console.log(isAuth);
let test = false;
return (
<Route
{...rest}
render={props =>
isAuth.isAuthenticated === true ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
);
}
const mapStateToProps = state => {
{
console.log(state);
return { isAuth: state.auth }; // change this line
}
};
export default withRouter(connect(mapStateToProps)(AuthRoute));

Testing an Authenticated Route using Jest/Enzyme

I have a component that only displays when the route is authenticated.
The way authentication is setup is with react-router v4 as an HOC:
Root.js
import React from 'react';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import reduxThunk from 'redux-thunk';
import reducers from './App/reducerIndex';
const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore);
export default ({children, initialState={}})=>{
return(
<Provider store={createStoreWithMiddleware(reducers, initialState)}>
{children}
</Provider>
);
}
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import reduxThunk from 'redux-thunk';
import reducers from './App/reducerIndex';
import { AUTH_USER } from './App/actionTypes';
import App from './App/app';
import '../style/css/custom.scss';
const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore);
const store = createStoreWithMiddleware(reducers);
const token = localStorage.getItem('token');
if (token) {
store.dispatch({ type: AUTH_USER });
}
import Root from './root';
ReactDOM.render(
<Root>
<App id={token}/>
</Root>,
document.querySelector('.app')
);
App.js
function PrivateRoute({ component: Component, auth, ...rest }) {
return (
<Route
{...rest}
render={props =>
auth ? (<div><Component {...props} /></div>) : (
<Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)
}
/>
);
}
function PublicRoute({ component: Component, auth, ...rest }) {
return <Route {...rest} render={props => (!auth ? <Component {...props} /> : <Redirect to="/dashboard" />)} />;
}
class App extends Component {
static contextTypes = {
router: PropTypes.object,
};
render() {
return (
<Router history={history} >
<Switch>
<PublicRoute auth={this.props.auth} path="/login" exact component={Login} />
<PrivateRoute auth={this.props.auth} path="/dashboard" exact component={Dashboard} />
</Switch>
</Router>
);
}
}
function mapStateToProps(state) {
return {
auth: state.auth.authenticated,
};
}
export default connect(
mapStateToProps,
null
)(App);
This is currently how the dashboard test is set up
dashboard test:
import React from 'react';
import { mount } from 'enzyme';
import Root from '../../../root';
import Header from './header';
import '../../../setupTests';
import Dashboard from './dashboard';
let wrapped;
beforeEach(() => {
const initialState = {
auth: true
};
wrapped = mount(
<Root initialState={initialState}>
<Dashboard />
</Root>
);
});
describe('Dashboard', () => {
console.log("The component is", wrapped)
// make our assertion and what we expect to happen
it('header component exists', () => {
expect(wrapped.contains(<Header />)).toBe(true)
});
it('renders without crashing', () => {
expect(component.contains(<Header />)).toBe(true)
});
});
Console.log keeps telling me the component is undefined. Would I have to wrap this around react-router? I an stumped.

React Private Route higher order component?

I am trying to make a PrivateRoute component for react. Here is my higher order component. Can you tell me what is the problem with this.
import React from "react";
import { Route, Redirect } from "react-router-dom";
import { connect } from "react-redux";
export default ({ component: Component, ...rest }) => {
class PrivateRoute extends React.Component {
render() {
console.log("This is private route called");
if (this.props.profile) {
return (
<Route
{...rest}
render={props =>
this.props.profile.loggedIn === true ? (
<Component {...props} />
) : (
<Redirect to="/login" />
)
}
/>
);
}
}
}
const mapStateToProps = state => ({
profile: state.profile
});
return connect(mapStateToProps)(PrivateRoute);
};
Here's how you can accomplish a protected route via a protected route component.
Working example: https://codesandbox.io/s/yqo75n896x
containers/RequireAuth.js
import React from "react";
import { Route, Redirect } from "react-router-dom";
import { connect } from "react-redux";
import ShowPlayerRoster from "../components/ShowPlayerRoster";
import ShowPlayerStats from "../components/ShowPlayerStats";
import Schedule from "../components/Schedule";
const RequireAuth = ({ match: { path }, isAuthenticated }) =>
!isAuthenticated ? (
<Redirect to="/signin" />
) : (
<div>
<Route exact path={`${path}/roster`} component={ShowPlayerRoster} />
<Route path={`${path}/roster/:id`} component={ShowPlayerStats} />
<Route path={`${path}/schedule`} component={Schedule} />
</div>
);
export default connect(state => ({
isAuthenticated: state.auth.isAuthenticated
}))(RequireAuth);
routes/index.js
import React from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import Home from "../components/Home";
import Header from "../containers/Header";
import Info from "../components/Info";
import Sponsors from "../components/Sponsors";
import Signin from "../containers/Signin";
import RequireAuth from "../containers/RequireAuth";
import rootReducer from "../reducers";
const store = createStore(rootReducer);
export default () => (
<Provider store={store}>
<BrowserRouter>
<div>
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/info" component={Info} />
<Route path="/sponsors" component={Sponsors} />
<Route path="/protected" component={RequireAuth} />
<Route path="/signin" component={Signin} />
</Switch>
</div>
</BrowserRouter>
</Provider>
);
Or, if you want something that wraps all routes (instead of having to specify a protected route component). Then you can do something like the below.
Working example: https://codesandbox.io/s/5m2690nn6n
components/RequireAuth.js
import React, { Component, Fragment } from "react";
import { withRouter } from "react-router-dom";
import Login from "./Login";
import Header from "./Header";
class RequireAuth extends Component {
state = { isAuthenticated: false };
componentDidMount = () => {
if (!this.state.isAuthenticated) {
this.props.history.push("/");
}
};
componentDidUpdate = (prevProps, prevState) => {
if (
this.props.location.pathname !== prevProps.location.pathname &&
!this.state.isAuthenticated
) {
this.props.history.push("/");
}
};
isAuthed = () => this.setState({ isAuthenticated: true });
unAuth = () => this.setState({ isAuthenticated: false });
render = () =>
!this.state.isAuthenticated ? (
<Login isAuthed={this.isAuthed} />
) : (
<Fragment>
<Header unAuth={this.unAuth} />
{this.props.children}
</Fragment>
);
}
export default withRouter(RequireAuth);
routes/index.js
import React from "react";
import { BrowserRouter, Switch, Route } from "react-router-dom";
import Home from "../components/Home";
import Players from "../components/Players";
import Schedule from "../components/Schedule";
import RequireAuth from "../components/RequireAuth";
export default () => (
<BrowserRouter>
<RequireAuth>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/players" component={Players} />
<Route path="/schedule" component={Schedule} />
</Switch>
</RequireAuth>
</BrowserRouter>
);
Or, if you want something a bit more modular, where you can pick and choose any route, then you can create a wrapper HOC. See this example (while it's written for v3 and not for authentication, it's still the same concept).
It looks like your render function's only return is inside of an if block, so its returning null. You need to fix the logic to just return the Route and make profile a required prop in your proptypes check instead of using an if block.
PrivateRoute.propTypes = {
profile: PropTypes.object.isRequired
};

Can't access my match.props in react router

I'm gonna post some code, because it appears I have not included something.
import React from 'react';
import { connect } from 'react-redux';
import {bindActionCreators} from 'redux';
import {updateSearch, getSearchResults} from '../actions/qb-actions';
import {valueAccepted} from '../actions/tree-actions';
import {addNewElement} from '../services/commonComponents';
import QueryBuilder from './QueryBuilder';
import SearchResults from './SearchResults';
import FontAwesome from 'react-fontawesome';
import {checkIncludeColumns} from '../services/commonComponents';
import {removeSpaces} from '../services/values';
import { Button } from 'reactstrap';
class SearchBooksAdvanced extends React.Component {
constructor(props) {
super(props);
this.state = {
};
}
componentDidMount(){
const { match: { params } } = this.props;
//I want to use match.params here, but it is coming back undefined.
}
qb=()=>{
}
postResults = () => {
this.qb();
}
render() {
return (<div >
<QueryBuilder postResults={this.postResults}/>
<SearchResults
qb={qb => {this.qb = qb}}
tree={this.props.tree.tree}
/>
</div>);
}
}
function mapStateToProps(state){
return{
options: state.options,
tree: state.tree,
goToSearch: state.goToSearch
};
}
export default connect(mapStateToProps, {updateSearch, getSearchResults})(SearchBooksAdvanced);
A little abbreviated class. Here is where I am calling my routings:
<main>
<Switch>
<AuthenticatedRoute login={this.props.login} path={`${this.props.match.path}`} exact component={Landing} isLoggedOut={this.props.login.ckUser.isLoggedOut}/>
<AuthenticatedRoute login={this.props.login} path={`${this.props.match.path}/search/:searchID`} exact component={SearchBooksAdvanced} isLoggedOut={this.props.login.ckUser.isLoggedOut}/>
<AuthenticatedRoute login={this.props.login} path={`${this.props.match.path}/book/:bookID`} exact component={BookDetail} isLoggedOut={this.props.login.ckUser.isLoggedOut}/>
<AuthenticatedRoute login={this.props.login} path={`${this.props.match.path}/routing/:routingID/:versionID`} exact component={RoutingDetail} isLoggedOut={this.props.login.ckUser.isLoggedOut}/>
<Redirect to={`${this.props.match.url}`} />
</Switch>
</main>
My AuthenicatedRoute class is as follows:
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
const AuthenticatedRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props =>
(
!props.isLoggedOut && localStorage.getItem('user') ?
<Component {...props}/>
:
<span><Redirect to={{
pathname: '/bis/login',
state: { from: props.location }
}}/><span>redirected</span></span>
)}/>
);
export default AuthenticatedRoute
What is weird here, is my props.match.params.someID worked for my other two classes, but when I called it for SeachBooksAdvanced class it fails to find it. Please let me know if you see an inconsistency in my code. TIA.
You want to use the withRouter HOC on route components that you use connect on.
import { withRouter } from "react-router-dom";
// ...
export default withRouter(
connect(
mapStateToProps,
{ updateSearch, getSearchResults }
)(SearchBooksAdvanced)
);

react-router-dom route component requires page reload

When calling history.push('/packages') the url is updated but the component will not mount (render) unless the page is reloaded. If I call createHistory({forceRefresh: true}) or manually reload the page the UI is rendered correctly. How can I configure react-router-dom to load the component without explicitly reloading the page or using forceRefresh?
index.jsx
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import store from './store'
import {Provider} from 'react-redux'
import App from './App'
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('app')
);
App.jsx
import React, { Component } from 'react'
import { Switch, Route } from 'react-router-dom'
import PropTypes from 'prop-types'
import { Navbar, PageHeader, Grid, Row, Col } from 'react-bootstrap'
import LoginFormContainer from './components/Login/LoginFormContainer'
import PackageContainer from './components/Package/PackageContainer'
class App extends Component {
render() {
return (
<Grid>
<Navbar>
<Navbar.Header>
<Navbar.Brand><h3>Mythos</h3></Navbar.Brand>
</Navbar.Header>
</Navbar>
<Row className="content">
<Col xs={12} md={12}>
<Switch>
<Route path='/packages' render={() => <PackageContainer />} />
<Route exact path='/' render={() => <LoginFormContainer />} />
</Switch>
</Col>
</Row>
</Grid>
)
}
}
export default App
loginActions.jsx
import * as types from './actionTypes'
import LoginApi from '../api/loginApi'
import createHistory from 'history/createBrowserHistory'
const history = createHistory()
export function loginUser(user) {
return function(dispatch) {
return LoginApi.login(user).then(creds => {
dispatch(loginUserSuccess(creds));
}).catch(error => {
throw(error);
});
}
}
export function loginUserSuccess(creds) {
sessionStorage.setItem('credentials', JSON.stringify(creds.data))
history.push('/packages')
return {
type: types.LOGIN_USER_SUCCESS,
state: creds.data
}
}
PackageContainer.jsx
import React, { Component } from 'react'
import {connect} from 'react-redux'
import PropTypes from 'prop-types'
import {loadPackages} from '../../actions/packageActions'
import PackageList from './PackageList'
import ImmutablePropTypes from 'react-immutable-proptypes'
import {Map,fromJS,List} from 'immutable'
import {withRouter} from 'react-router-dom'
class PackageContainer extends Component {
constructor(props, context) {
super(props, context);
}
componentDidMount() {
this.props.dispatch(loadPackages());
}
render() {
return (
<div className="col-lg-12">
{this.props.results ?
<PackageList results={this.props.results} /> :
<h3>No Packages Available</h3>}
</div>
);
}
}
PackageContainer.propTypes = {
results: ImmutablePropTypes.list.isRequired,
};
const mapStateToProps = (state, ownProps) => {
return {
results: !state.getIn(['packages','packages','results']) ? List() : state.getIn(['packages','packages','results'])
};
}
PackageContainer = withRouter(connect(mapStateToProps)(PackageContainer))
export default PackageContainer
I assume, that issue that you are create new instance of history object but BrowserRouter doesn't know about the changes which happens inside of it.
So, you should create history object and export it in the index.jsx and use Router instead of BrowserRouter and pass as history property, so then you can just import it whenever you need.
For example:
index.jsx
import { Router } from 'react-router-dom'
import createHistory from 'history/createBrowserHistory'
...
export const history = createHistory()
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<App />
</Router>
</Provider>,
document.getElementById('app')
);
Then, in loginActions you just import history and use .push method as before.
loginActions.jsx
import * as types from './actionTypes'
import LoginApi from '../api/loginApi'
import { history } from './index'
export function loginUser(user) {
return function(dispatch) {
return LoginApi.login(user).then(creds => {
dispatch(loginUserSuccess(creds));
}).catch(error => {
throw(error);
});
}
}
export function loginUserSuccess(creds) {
sessionStorage.setItem('credentials', JSON.stringify(creds.data))
history.push('/packages')
return {
type: types.LOGIN_USER_SUCCESS,
state: creds.data
}
}
Hope it will helps.
In the App.jsx
<Switch>
<Route path='/packages' render={(props) => <PackageContainer {...props}/>} />
<Route exact path='/' render={(props) => <LoginFormContainer {...props}/>} />
</Switch>
Now both PackageContainer and LoginFormContainer have access to history object

Resources