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);
Related
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);
I am not able to send the parameter through state using useHistory history.push method from react-router dom.
Now suppose I want to pass more than a string to the Paging component i.e. some props too.
My Paging Component which throws error for state value state is not defined
const PAGING = ({ location }) => {
console.log(location);
console.log(location.state);
console.log(location.state.id);
return <div>Hello <div>}
History.push method in another component
const handleDetails = (id,name) => {
console.log(name)
if (id) {
return history.push({
pathname: `/detailing/${name}`,
state: { id }
});
} else {
return history.push("/");
}
};
const Switch = () => {
const { state: authState } = useContext(AuthContext)
return (
<div>
<Router>
<Switch>
<ProtectedSystem
path= "/detailing/:name"
exact
auth={authState.isAuthenticated}
component={PAGING}
/>
</Switch>
</Router>
</div>
);
const ProtectedSystem = ({auth , component: Component, ...rest}) =>{
return(
<Route
{...rest}
render={() => auth ? (<Component/>) : (<Redirect to = '/' /> )}
/>
)
}
If I use simple route without condition based its working fine
<Route path= "/detailing/:name" exact component={PAGING} />
You need to pass on the Route params to the rendered component so that it can use them
const ProtectedSystem = ({auth , component: Component, ...rest}) =>{
return(
<Route
{...rest}
render={(routeParams) => auth ? (<Component {...routeParams}/>) : (<Redirect to = '/' /> )}
/>
)
}
You can do this entirely with React hooks and pure functions, eg.
import React from 'react';
import { useHistory } from 'react-router-dom';
const ProtectedSystem = ({ auth }) => {
const history = useHistory();
if (!authUser) {
history.push("/signin");
}
return (
<div><h1>Authorized user</h1></div>
)
}
export default ProtectedSystem
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
I'm setting up a basic authentication system with React and while signup and login actions correctly redirect and render the appropriate components, my logout action redirects to the protected route and renders the associated component, even though the authentication variable managed with the context API is successfully updated when logging out. The whole operation works in the end, as when I'm refreshing the page, I am successfully redirected to my login page.
I'm using Node.js to manage my sessions and dispatching the logout action works well as, as I said, the variable used with the Context API is updated. I'm using the Effect Hook on my Header component where the logout is initiated and I can see the auth variable being changed.
Here is my code:
AppRouter.js
export const history = createBrowserHistory();
const AppRouter = () => (
<Router history={history}>
<Switch>
<PublicRoute path="/" component={AuthPage} exact={true} />
<PrivateRoute path="/dashboard" component={DashboardPage} />
<Route component={NotFoundPage} />
</Switch>
</Router>
);
PublicRoute.js
const PublicRoute = ({ component: Component, ...rest }) => {
const { uid } = useContext(AuthContext);
useEffect(() => {
console.log("Public Route - Variable set to:", uid);
}, [uid])
return (
<Route
render={props =>
uid !== undefined ? (
<Redirect to="/dashboard" />
) : (
<Component {...props}/>
)
}
{...rest}
/>
)
};
PrivateRoute.js
const PrivateRoute = ({ component: Component, ...rest }) => {
const { uid } = useContext(AuthContext);
useEffect(() => {
console.log("Private Route - Variable set to:", uid);
}, [uid])
return (
<Route
render={props =>
uid !== undefined ? (
<div>
<Header />
<Component {...props}/>
</div>
) : (
<Redirect to="/" />
)
}
{...rest}
/>
)
};
Header.js
export const Header = () => {
const { uid, dispatch } = useContext(AuthContext);
useEffect(() => {
console.log("Header - Variable set to:", uid);
// console.log("HIST", history);
}, [uid])
const logout = async () => {
const result = await startLogout();
if (result.type !== undefined) {
dispatch(result); // Works well
// window.location.href = '/';
// history.push('/');
history.replace('/');
} else {
console.log(result);
}
}
return (
<header className="header">
<div className="container">
<div className="header__content">
<Link className="header__title" to="/dashboard">
<h1>A React App</h1>
</Link>
<button className="button button--link" onClick={logout}>Logout</button>
</div>
</div>
</header>
);
};
I tried both history.push('/') and history.replace('/'). Both these 2 methods work well as if I switch the path to an unknown route, my component that handles 404 is successfully rendered.
Below is my console output when I click the logout button. As you can see, the auth variable is well updated to undefined but that does not prevent my router to keep showing me the protected route. The router should not redirect me to the dashboard as my auth variable is set to undefined after logging out.
Header - Variable set to: {uid: undefined}
Private Route - Variable set to: {uid: undefined}
Public Route - Variable set to: {uid: undefined}
Header - Variable set to: {uid: undefined}
Private Route - Variable set to: {uid: undefined}
For the time being I'm using window.location.href = '/'; which works well, as it automatically reload the root page but I'd like to stick to react-router. Any thoughts? Thanks
in the private route pass renders props.. like this:
const PrivateRoute = ({ component: Component, ...rest }) => {
const { uid } = useContext(AuthContext);
useEffect(() => {
console.log("Private Route - Variable set to:", uid);
}, [uid])
return (
<Route
render={props =>
uid !== undefined ? (
<div>
<Header {...props} />
<Component {...props}/>
</div>
) : (
<Redirect to="/" />
)
}
{...rest}
/>
)
};
then in header use props to push history:
export const Header = (props) => {
const { uid, dispatch } = useContext(AuthContext);
useEffect(() => {
console.log("Header - Variable set to:", uid);
// console.log("HIST", history);
}, [uid])
const logout = async () => {
const result = await startLogout();
if (result.type !== undefined) {
dispatch(result); // Works well
// window.location.href = '/';
// history.push('/');
props.history.push('/');
} else {
console.log(result);
}
}
I'm getting this compile error, but I'm unclear about what's undefined. This all looks like it should work, or at least not produce errors, but maybe I'm missing something obvious? Any assistance would be much appreciated. I'm happy to upload any additional code that might shed some light on the problem.
←→1 of 2 errors on the page
TypeError: Cannot read property 'map' of undefined
Persons.render
src/Components/Persons.jsx:14
11 | }
12 |
13 | render() {
> 14 | let personsList = this.props.persons.map( ( persons, index) => {
| ^ 15 | return <div className="personsListLinks"><li key={index}><Link to={`/api/persons/${persons.id}`}>{persons.name}</Link></li></div>
16 | })
17 | return (
View compiled
This is the Component:
import { BrowserRouter as Link } from "react-router-dom";
class Persons extends Component {
constructor(props){
super(props)
this.state = {
persons: '',
personsList: ''
}
}
render() {
let personsList = this.props.persons.map( ( persons, index) => {
return <div className="personsListLinks"><li key={index}><Link to={`/api/persons/${persons.id}`}>{persons.name}</Link></li></div>
})
return (
<div className="PersonsList">
<h1>Persons</h1>
{personsList}
</div>
)
}
}
export default Persons;
And, App.jsx...
import './App.css';
import Signup from './Signup';
import Login from './Login';
import UserProfile from './UserProfile';
import { BrowserRouter as Router, Route, Link, Redirect } from "react-router-dom";
import axios from 'axios';
import PersonsShow from './Components/PersonsShow';
import Persons from './Components/Persons';
import { set } from 'mongoose';
class App extends Component {
constructor(props) {
super(props)
this.state = {
token: '',
user: null,
errorMessage: '',
lockedResult: '',
persons: [],
personality: [],
quotes: []
}
this.liftTokenToState = this.liftTokenToState.bind(this)
this.checkForLocalToken = this.checkForLocalToken.bind(this)
this.logout = this.logout.bind(this)
this.handleClick = this.handleClick.bind(this)
this.addNewPerson = this.addNewPerson.bind(this)
// this.addQuote = this.addQuote.bind(this)
}
checkForLocalToken() {
// Look in localStorage for the token
let token = localStorage.getItem('mernToken')
if (!token || token === 'undefined') {
// There is no token
localStorage.removeItem('mernToken')
this.setState({
token: '',
user: null
})
} else {
// Found a token, send it to be verified
axios.post('/auth/me/from/token', {token})
.then( res => {
if (res.data.type === 'error') {
localStorage.removeItem('mernToken')
this.setState({errorMessage: res.data.message})
} else {
// Put token in localStorage
localStorage.setItem('mernToken', res.data.token)
// Put token in state
this.setState({
token: res.data.token,
user: res.data.user
})
}
})
}
}
componentDidMount() {
this.checkForLocalToken()
this.getPersons()
}
getPersons = () => {
axios.get('/persons')
.then(res => {
this.setState({
persons: res.data
})
})
}
liftTokenToState({token, user}) {
this.setState({
token,
user
})
}
logout(){
// Remove the token from localStorage
localStorage.removeItem ('mernToken')
// Remove the user and token from state
this.setState({
token: '',
user: null
})
}
handleClick(e) {
e.preventDefault()
// axios.defaults.headers.common['Authorization'] = `Bearer ${this.state.token}`
let config = {
headers: {
Authorization: `Bearer ${this.state.token}`
}
}
axios.get('/locked/test', config).then( res => {
// console.log("this is the locked response:" , res);
this.setState({
lockedResult: res.data
})
})
}
handleNewPersonClick(person) {
this.setState({
person: person
})
}
addNewPerson(e) {
e.persist()
e.preventDefault()
let user = this.props.user
let person = this.props.persons
axios.post(`/api/user/:id/person${user._id}/`, {
name: e.target.name
}).then(res => {
axios.get(`/api/user/${user._id}/persons`).then(res => {
this.setState({persons: res.data})
})
})
}
render() {
let user = this.state.user
let contents;
if (user) {
contents = (
<Router>
<Route exact path='/' render= {() => <Redirect to='/persons' /> } />
<Route exact path='/' render={() => <Persons persons={this.state.persons} user={this.state.user} logout={this.logout} />}/>
{/* <Route path="/persons/:pid" render={(props) => <PersonsShow person={this.state.persons} addItem={this.addItem} user={user} logout={this.logout} {...props} />}/> */}
{/* <Route path="/cart" render={() => <CartPage cart={this.state.cart} />}/> */}
<UserProfile user={user} logout={this.logout} />
<Persons person={this.state.persons} addItem={this.addItem} user={user} logout={this.logout}/>
{/* <PersonsShow user={user} /> */}
{/* <Persons user={user} /> */}
{/* <p><a onClick={this.handleClick}>Test the protected route...</a></p> */}
<p>{this.state.lockedResult}</p>
</Router>
)
} else {
contents = (
<>
<Signup liftToken={this.liftTokenToState} />
<Login liftToken={this.liftTokenToState} />
</>
)
}
return (
<div className="App">
<header><h1>Welcome to Judge-O-Matic!</h1></header>
<div className="content-box">
{contents}
</div>
</div>
)
}
}
export default App;
I've tried rewriting as a functional component with similar results.
You have a typo:
<Persons person={this.state.persons} addItem={this.addItem} user={user} logout={this.logout}/>
should be
<Persons persons={this.state.persons} addItem={this.addItem} user={user} logout={this.logout}/>
Please note the s at the end of the first prop.