Private route get Authentication before render Component - reactjs

I have problem in using PrivateRoute for user with authentication.
Since I use redux to catch authentication of an user, I need to wait state return from store before rendering component.
If not, the page will be automatically redirect to login route.
My problem is that current route will be redirect to login page while waiting authentication result. It will be return page if authenticated.
So I want wait for authentication result first. If authenticated, page component will be loaded, if not it will redirect to login page.
Here is my private route with authentication.
import React from "react";
import { Route, Redirect} from "react-router-dom";
import { connect } from "react-redux";
const PrivateRoute = ({ component: Component, auth, role, ...rest }) => {
return (
<Route
{...rest}
render={props => auth.isAuthenticated ? (
<Component {...props} />
) : (
<Redirect to={{
pathname: "/",
state: { from: props.location }
}} />
)
}
/>
)
}
const stateStoreToProp = state => ({
auth: state.loginReducer,
});
export default connect(stateStoreToProp)(PrivateRoute);
Here is my login Component for auto redirect.
let history = useHistory();
let location = useLocation();
let { from } = location.state || { from: { pathname: "/choose" } };
const isAuthenticated = prop.loginReducer.isAuthenticated;
const user = prop.loginReducer.user;
if (isAuthenticated && typeof user !== "undefined") {
history.replace(from);
}
And here is App.js
import { loadUser } from './actions/AuthAction';
function App() {
useEffect(() => {
store.dispatch(loadUser());
}, []);
return (
<Provider store={store}>
<Router>
<Fragment>
<div className="blockcontain">
<div className="fss">
<div className="rightSide">
<Switch>
<Route path='/' exact component={Login} />
<PrivateRoute exact path='/choose' component={Choose} />
</Switch>
</div>
</div>
</div>
</Fragment>
</Router>
</Provider>
);
}
and LoginReducer
import {
LOGIN_SUCCESS, LOGIN_FAIL, USER_LOADED, AUTH_ERROR
} from '../actions/typeName';
import Cookies from 'js-cookie';
const initialState = {
token: Cookies.get('token'),
isAuthenticated: null,
loading: true,
user: null
}
const loginReducer = (state = initialState, action) => {
const {type, payload} = action;
switch(type) {
case USER_LOADED:
return {
...state,
isAuthenticated: true,
loading: false,
user: payload
};
case LOGIN_SUCCESS:
Cookies.set('token', payload.access_token, { expires: payload.expires_at });
return {
...state,
isAuthenticated: true,
loading: false
}
case AUTH_ERROR:
case LOGIN_FAIL:
return {
...state,
token:null,
isAuthenticated: false,
loading: true
}
default:
return state;
}
}
export default loginReducer;
I go choose page, it will go to login page because the store for authentication is not ready.
Thank you.

You could add a loading property to your state and only show the route when fetching has been completed:
const PrivateRoute = ({ component: Component, auth, isLoading, role, ...rest }) => {
if(isLoading) return <div>Loading...</div>;
return (
<Route
{...rest}
render={(props) =>
auth.isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: '/',
state: { from: props.location },
}}
/>
)
}
/>
)
}
const stateStoreToProp = (state) => ({
isLoading: state.loading,
auth: state.loginReducer,
})
export default connect(stateStoreToProp)(PrivateRoute)
And also add the loading mutations in your reducer, for example:
case REQUEST_LOGIN:
return [...state, isLoading: true];
case LOGIN_SUCCESSFUL:
return [...state, auth: isAuthenticated, isLoading: false];

What you can do is to add a "loading" key to your auth state, so while it is authenticating the state will be "true" else "false". If it is true you display a loader.
import React from "react";
import { Route, Redirect} from "react-router-dom";
import { connect } from "react-redux";
const PrivateRoute = ({ component: Component, auth, role, ...rest }) => {
return (
<>
{
auth.loading && (
<Loader/>
)
}
{
!auth.loading && (
(
<Route
{...rest}
render={props => auth.isAuthenticated ? (
<Component {...props} />
) : (
<Redirect to={{
pathname: "/",
state: { from: props.location }
}} />
)
}
/>
)
)
}
</>
)
}
const stateStoreToProp = state => ({
auth: state.loginReducer,
});
export default connect(stateStoreToProp)(PrivateRoute);

Related

Prevent user from directly accessing URL in React application?

Is there a way to stop the user from directly accessing a URL on my application? For example, we have a page that is accessed as localhost:3000/scheduling but I want to re-route back to the homepage. I couldn't find many helpful articles that could achieve this. I am using React by the way.
Thanks!
You can do it in many ways, this is just an example :
const location = useLocation();
let history = useHistory();
if(location.state == undefined || location.state == null || location.state == ''){
history.push("/");
}
'/' is by default your home page.
You can check this example:
import React from 'react'
import {
BrowserRouter as Router,
Route,
Link,
Redirect,
withRouter
} from 'react-router-dom'
const fakeAuth = {
isAuthenticated: false,
authenticate(cb) {
this.isAuthenticated = true
setTimeout(cb, 100)
},
signout(cb) {
this.isAuthenticated = false
setTimeout(cb, 100)
}
}
const Public = () => <h3>Public</h3>
const Protected = () => <h3>Protected</h3>
class Login extends React.Component {
state = {
redirectToReferrer: false
}
login = () => {
fakeAuth.authenticate(() => {
this.setState(() => ({
redirectToReferrer: true
}))
})
}
render() {
const { from } = this.props.location.state || { from: { pathname: '/' } }
const { redirectToReferrer } = this.state
if (redirectToReferrer === true) {
return <Redirect to={from} />
}
return (
<div>
<p>You must log in to view the page</p>
<button onClick={this.login}>Log in</button>
</div>
)
}
}
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
fakeAuth.isAuthenticated === true
? <Component {...props} />
: <Redirect to={{
pathname: '/login',
state: { from: props.location }
}} />
)} />
)
export default function AuthExample () {
return (
<Router>
<div>
<ul>
<li><Link to="/public">Public Page</Link></li>
<li><Link to="/protected">Protected Page</Link></li>
</ul>
<Route path="/public" component={Public}/>
<Route path="/login" component={Login}/>
<PrivateRoute path='/protected' component={Protected} />
</div>
</Router>
)
}
Source
We can use Conditional rendering tracing the history.
You can also add conditions using this.props.history.location.key or this.props.history.action
Key exists and action is 'PUSH' when we redirect user using this.props.history.push
Key property doesn't exist and action is 'POP' when a user tries to access the URL directly
return this.props.history.location.key ? (<div></div>) : null

How to pass props in the modal in react

I am creating my first react project, i am using GitHub api to fetch user and display them firstly in card view then on clicking on more button to any profile i want to create a modal using portals in react till now i am able to create an modal but now i am not getting how to get data to that modal coponent
Here is my App.js
import React, { Fragment, Component } from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import Navbar from './components/layout/Navbar';
import Users from './components/users/Users';
import User from './components/users/User';
import Modal from './components/Modal/Modal'
import Search from './components/users/Search';
import Alert from './components/layout/Alert';
import About from './components/pages/About';
import axios from 'axios';
import './App.css';
class App extends Component {
state = {
users: [],
user: {},
loading: false,
alert: null,
modal: {},
}
// get users from Search.js
searchUsers = async text => {
this.setState({ loading: true })
const res = await axios.get(
`https://api.github.com/search/users?q=${text}&client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID
}&client_secret=${process.env.REACT_APP_GITHUB_CLIENT_SECRET}`);
this.setState({ users: res.data.items, loading: false })
console.log(text);
}
//get single profile
getUser = async username => {
this.setState({ loading: true })
const res = await axios.get(
`https://api.github.com/users/${username}?client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID
}&client_secret=${process.env.REACT_APP_GITHUB_CLIENT_SECRET}`);
this.setState({ user: res.data, loading: false });
this.setState({ modal: res.data, loadading: false });
}
//clear search
clearUsers = () => this.setState({ users: [], loading: false });
setAlert = (msg, type) => {
this.setState({ alert: { msg: msg, type: type } });
setTimeout(() => this.setState({ alert: null }), 5000);
};
render() {
return (
<Router>
<div className='App'>
<Navbar />
<div className="container">
<Alert alert={this.state.alert} />
<Switch>
<Route exact path='/'
render={props => (
<Fragment>
<Search
searchUsers={this.searchUsers}
clearUsers={this.clearUsers}
showClear={this.state.users.length > 0 ? true : false}
setAlert={this.setAlert}
/>
<Users loading={this.state.loading} users={this.state.users} />
</Fragment>
)} />
<Route path='/about' component={About} />
<Route path='/user/:login' render={props => (
<User {...props} getUser={this.getUser} user={this.state.user} loading={this.state.loading} />
)} />
<Route path='/modal/:login' render={props => (
<Modal {...props} getUser={this.getUser} modal={this.state.modal} loading={this.state.loading} />
)} />
</Switch>
</div>
</div>
</Router>
);
}
}
export default App;
here is my Modal.js
import React, { Fragment, Component } from 'react';
import ReactDom from 'react-dom';
import Spinner from '../layout/Spinner';
import { Link } from 'react-router-dom';
const modalRoot = document.getElementById('modal');
export default class Modal extends Component {
constructor() {
super();
this.el = document.createElement('div');
}
componentDidMount = () => {
modalRoot.appendChild(this.el);
};
componentWillUnmount = () => {
modalRoot.removeChild(this.el);
};
render() {
const {
children,
name,
avatar_url,
location,
bio,
blog,
followers,
following,
public_repos,
} = this.props.modal;
const { loading } = this.props;
if (loading) return <Spinner />
return (
ReactDom.createPortal(children, this.el)
)
}
}
any guide would be appriciated thanks in advance
You are passing the props already to Modal.
In Modal, do something like
Class Modal extends Component {
constructor(){
super(props);
}
render(){
const {
modal,
getUser,
loading,
anyOtherPropYouPassIn
} = this.props;
const { loading } = this.props;
if (loading) return <Spinner />
return (
ReactDom.createPortal(children, this.el)
)
}

Update context value after API call

I'm using React 16.3 Context API, I'm setting loggedin: bool & user: Object value using context, also using PrivateRoute for logged in user.
Here is a brief code.
// AuthContext JS
import React from "react";
const AuthContext = React.createContext();
class AuthProvider extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoggedIn: false,
user : null
};
this.setAuth = this.setAuth.bind(this);
};
setAuth(isLoggedIn = false, userData = null) {
this.setState({
isLoggedIn: isLoggedIn,
user : userData
});
}
render() {
return (
<AuthContext.Provider
value={ {...this.state, setAuth: this.setAuth} }>
{ this.props.children }
</AuthContext.Provider>
);
}
}
const AuthUser = AuthContext.Consumer;
export {AuthContext, AuthProvider, AuthUser};
function PrivateRoute({component: Component, ...rest}) {
return (
<AuthUser>
{
({isLoggedIn}) => (
<Route
{ ...rest }
render={ props =>
(
isLoggedIn ? (
<Component { ...props } />
) : (
<Redirect
to={ {
pathname: "/login",
state : {from: props.location}
} }
/>
)
)
}
/>
)
}
</AuthUser>
);
}
// App JS
class App extends Component {
render() {
return (
<HashRouter>
<AuthProvider>
<Switch>
<Route exact path="/login" name="Login Page" component={ Login } />
<Route exact path="/register" name="Register Page" component={ Register } />
<Route exact path="/404" name="Page 404" component={ Page404 } />
<Route exact path="/500" name="Page 500" component={ Page500 } />
<PrivateRoute path="/" component={ DefaultLayout } />
</Switch>
</AuthProvider>
</HashRouter>
);
}
}
export default App;
// Login JS
class Login extends Component {
handleSubmit(values) {
const opts = {
"email" : "test#example.com",
"password": "test123"
};
let _this = this;
fetch("API_URL", {
method: "post",
body : JSON.stringify(opts)
})
.then(
(response) => {
return response.json();
}
).then(
(data) => {
_this.setState({
isAuth: true,
user : data.data.user
});
_this.props.history.replace("/dashboard");
}
);
}
render() {
console.log(this.state.isAuth);
return (
<AuthUser>
{
({isLoggedIn, setAuth}) =>
(
<Redirect to="/dashboard" />
) : ( <div > // Login Page </div>
)
}
</AuthUser>
);
}
}
How do I update/call setAuth function of consumer
If I call setAuth from render function, it will give warning & loop over setState
Any Help!
In the handleSubmit function in the Login file, instead of calling
this.setState({
isAuth: true,
user: data.data.user
});
you should call the setAuth function provided by the context and update the user auth and data in the context there:
this.context.setAuth(true, data.data.user)
In order to use this.context, you may need to change from using context consumer to contextType:
static contextType = AuthContext
You have implement a higher order component that help component consume context value as props.
The following withContextAsProps HOC provides an example:
function withContextAsProps(Context, Component) {
function WithContextAsProps(prop) {
return (
<Context>
{value => <Component {...value} />}
</Context>
);
}
const componentName = Component.displayName || Component.name || 'Component';
const contextName = Context.displayName || Context.name || 'Context';
WithContextAsProps.displayName = `With${contextName}Context(${componentName})`;
return WithContextAsProps;
}
In Login component, the HOC can be used to make isAuth and setAuth value from AuthUser context consumer available as props in the Login component.
class Login extends Component {
handleSubmit = values => {
//...
fetch("API_URL", {
method: "post",
body : JSON.stringify(opts)
})
.then(response => response.json())
.then(
data => {
this.props.setAuth(true, data.data.user);
this.props.location.assign("/dashboard");
}
);
}
render() {
return this.props.isAuth ?
<Redirect to="/dashboard" />
: <div>Login Page</div>;
}
}
export default withContextAsProps(AuthUser, Login);

react router redirects but does not render with redux

I am trying to make a login page which will redirect to the home page after successful authentication. Here is the code.
routes.js
const createRoutes = (store, history) => {
return (
<Router history={history}>
<div>
<AppLayout/>
<Route exact path="/" component={Home}/>
<Route path="/login" component={LoginContainer}/>
<Route path="/register" component={RegisterContainer}/>
</div>
</Router>
);
};
export default createRoutes;
actions.js
export const login = (data, successPath) => {
return (dispatch) => {
dispatch(beginLogin());
return makeUserRequest("post", "/login", data)
.then((resp) => {
if (resp.data.success) {
dispatch(loginSuccess(data));
browserHistory.push(successPath);
} else {
dispatch(loginError(resp.data));
}
})
.catch(console.error);
};
};
Login.js
class Login extends React.Component {
login = (event) => {
event.preventDefault();
const email = event.target.querySelector("input[name='email']").value;
const password = event.target.querySelector("input[name='password']").value;
this.props.login({
email,
password
}, this.props.nextPathName);
}
render() {
return (
<div className="Login">
<h2>Login</h2>
<form onSubmit={this.login}>
<input type="email" name="email" placeholder="Email"/>
<input type="password" name="password" placeholder="Password"/>
<input type="submit" value="Login"/>
<h1>{this.props.user.email}</h1>
</form>
</div>
);
}
}
Login.propTypes = {
login: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
nextPathName: PropTypes.string.isRequired
};
export default Login;
LoginContainer.js
const mapStateToProps = (state, ownProps) => {
let nextPathName = "/";
try {
nextPathName = ownProps.location.state.nextPathName;
} catch(err) {
// ignore
}
return {
user: state.user,
nextPathName
};
};
export default withRouter(connect(mapStateToProps, userActions)(Login));
reducer.js
// reducer for user actions
const user = (state = { isWaiting: false, authenticated: false, email: "", message: "" }, action) => {
switch(action.type) {
case REGISTER_USER:
return { ...state, isWaiting: true };
case REGISTER_SUCCESS_USER:
return { ...state, isWaiting: false, message: action.data.message };
case REGISTER_ERROR_USER:
return { ...state, isWaiting: false, message: action.data.message };
case LOGIN_USER:
return { ...state, isWaiting: true };
case LOGIN_SUCCESS_USER:
return { ...state, isWaiting: false, authenticated: true, email: action.data.email };
case LOGIN_ERROR_USER:
return { ...state, isWaiting: false };
default:
return state;
}
};
export default user;
export default combineReducers({
user,
routing: routerReducer
});
The login functionality is working correctly and when I am clicking the login button, the email in the component is printing correctly. After that the browser is changing to the new location which is "/", but the page is not being rendered, i.e. the view is remaining same even after the browser location has changed. I have looked into similar question answers on stackoverflow, but have not found any solution to my problem. I have even used withRouter but to no solution. Any help will be appreciated.
I think you are supposed to use one of the higher level Router components like : <BrowserRouter> if your web/app is hosted on a dynamic server
or <HashRouter> if your webs/app is hosted on a static server
routes.js
import {BrowserRouter, Route} from 'react-router-dom';
const createRoutes = (store, history) => {
return (
<BrowserRouter>
<div>
<AppLayout/>
<Route exact path="/" component={Home}/>
<Route path="/login" component={LoginContainer}/>
<Route path="/register" component={RegisterContainer}/>
</div>
<BrowserRouter>
);
};
export default createRoutes;
<BrowserRouter> is just a wrapper with a pre-built history so you can do same with custom history instance . You can read more Here
Instead of using the imperative push method, you could use the nice declarative <Redirect /> component that comes with React Router:
{user.authenticated && <Redirect to={nextPath} />}
You can place this wherever it makes sense for you. Inside your Login component or at the top level (just making sure not to redirect over and over)

React Router 4 Match returns undefined

I'm using react router 4 and I'm having trouble accessing the id from a url using params. I've followed the react router 4 documentation however when I console.log match.params.id it returns Cannot read property 'params' of undefined. The URL contains the id so I'm lost. You can find the console.log in Path: Container
What am I doing wrong?
Path: App
const App = appProps => (
<Router>
<div className="bgColor">
<NavBar {...appProps} />
<Grid className="main-page-container">
<Switch>
<Admin exact path="/admin/candidate_profile/:id" component={AdminCandidateProfileContainer} {...appProps} />
</Switch>
</Grid>
</div>
</Router>
);
App.propTypes = {
loggingIn: PropTypes.bool,
authenticatedCandidate: PropTypes.bool,
authenticatedAdmin: PropTypes.bool
};
export default createContainer(() => {
const loggingIn = Meteor.loggingIn();
return {
loggingIn,
authenticatedCandidate: !loggingIn && !!Meteor.userId() && !!Roles.userIsInRole(Meteor.userId(), 'Candidate'),
authenticatedAdmin: !loggingIn && !!Meteor.userId() && !!Roles.userIsInRole(Meteor.userId(), 'Admin')
};
}, App);
Path: AdminRoute
const Admin = ({ loggingIn, authenticatedAdmin, component: Component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if (loggingIn) return <div />;
return authenticatedAdmin ?
(<Component loggingIn={loggingIn} authenticatedAdmin={authenticatedAdmin} {...rest} />) :
(<Redirect to="/login" />);
}}
/>
);
Admin.propTypes = {
loggingIn: PropTypes.bool,
authenticatedAdmin: PropTypes.bool,
component: PropTypes.func
};
export default Admin;
Path: Container.js
export default CandidateProfileContainer = createContainer(({ match }) => {
console.log('match', match.params.id);
const profileCandidateCollectionHandle = Meteor.subscribe('admin.candidateProfile');
const loading = !profileCandidateCollectionHandle.ready();
const profileCandidateCollection = ProfileCandidate.findOne({ userId: Meteor.userId() });
const profileCandidateCollectionExist = !loading && !!profileCandidateCollection;
return {
loading,
profileCandidateCollection,
profileCandidateCollectionExist,
profileCandidate: profileCandidateCollectionExist ? profileCandidateCollection : {}
};
}, CandidateProfilePage);
You're not passing props from render
const Admin = ({ loggingIn, authenticatedAdmin, component: Component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if (loggingIn) return <div />;
return authenticatedAdmin ?
(<Component
loggingIn={loggingIn}
authenticatedAdmin={authenticatedAdmin}
{...rest}
{...props} <--- match, location are here
/>) :
(<Redirect to="/login" />);
}}
/>
);

Resources