I m trying to re-route the user to the url that the user was trying to navigate on the browser address bar by doing this:
I checked if a user is logged in by calling a service method. If the session in the cookie is expired or not valid, it will return a 401 status and I will redirect to the login screen
If user is logged in, allow.
If user is not logged in, route to the login screen and after login, route to the desired url.
The issue here is, when a user type a url like : http://url/app/order
it gets redirected to the login URL : http://url/auth/login
After the user enters his credentials, although the action gets dispatched, the render for the Authorizedroute component is not called. It gets called after I click the Login button again.
Following is my Login Component
export class LoginForm extends React.Component {
componentWillReceiveProps(newProps){
const { location, isAuthenticated, history } = newProps;
if(isAuthenticated){
if(location.state && location.state.referrer){
history.push(location.state.referrer.pathname);
}else{
history.replace('/app');
}
}
}
render() {
let usernameInput, passwordInput;
const { showErrorMessage, errorMessage, onLogin } = this.props;
return (
<div className='sme-login-center-view-container'>
<HeaderComponent />
<Col lg={4} lgOffset={4} sm={12} xs={12}>
<Col lg={10} lgOffset={2} sm={4} smOffset={4}>
<Form className="sme-login-box" onSubmit={(e)=> {
e.preventDefault();
let creds = {username: usernameInput.value, password: passwordInput.value};
onLogin(creds);
}
}>
<span className='text-center sme-login-title-text'><h4>User Login</h4></span>
<FormGroup >
{errorMessage ? (<Alert bsStyle="danger"><strong>Error!</strong> {errorMessage}</Alert>) : null }
</FormGroup>
<FormGroup controlId="formHorizontalUsername">
<FormControl type="username" placeholder="Username" bsStyle="form-rounded"
inputRef={(ref) => {usernameInput = ref}}
/>
<FormControl.Feedback>
<span className="fa fa-user-o sme-login-input-feedback-span"></span>
</FormControl.Feedback>
</FormGroup>
<FormGroup controlId="formHorizontalPassword">
<FormControl type="password" placeholder="Password"
inputRef={(ref) => {passwordInput = ref}}/>
<FormControl.Feedback>
<span className="fa fa-lock sme-login-input-feedback-span"></span>
</FormControl.Feedback>
</FormGroup>
<FormGroup>
<Button type="submit" block >Login</Button>
</FormGroup>
</Form>
</Col>
</Col>
</div>
);
}
}
LoginContainer
const mapStateToProps = state => {
return state.authenticationReducer.login
}
export const Login = withRouter(connect( mapStateToProps,{ onLogin: loginUser })(LoginForm))
Login Action
export function requestLogin(creds) {
return {
type: LOGIN_REQUEST,
isFetching: true,
isAuthenticated: false,
creds
}
}
export function receiveLogin() {
return {
type: LOGIN_SUCCESS,
isFetching: false,
isAuthenticated: true
}
}
export function loginError(message) {
return {
type: LOGIN_FAILURE,
isFetching: false,
isAuthenticated: false,
errorMessage: message
}
}
export function loginUser(creds) {
return dispatch => {
dispatch(requestLogin(creds))
return axios.post(url, data)
.then(response => {
if (!response.status === 200) {
dispatch(loginError(response.statusText))
} else {
dispatch(receiveLogin())
}
}
).catch(function (error) {
dispatch(loginError(error.response.statusText))
}
) }
}
Login Reducer:
export function login(state = {
isFetching: false,
isAuthenticated: false
}, action) {
switch (action.type) {
case LOGIN_REQUEST:
case LOGIN_SUCCESS:
case LOGIN_FAILURE:
return Object.assign({}, state, action)
default:
return state
}
}
Authorized Route Component
class AuthorizedRouteComponent extends React.Component {
componentWillMount() {
this.props.getUser();
}
render() {
const { component: Component, pending, logged, location, ...rest } = this.props;
return (
<Route {...rest} render={props => {
if (pending) return <div>Loading...</div>
return logged
? <Component {...this.props} />
:<Redirect to={{
pathname: '/auth/login',
state: { referrer: location }
}}/>
}} />
)
}
}
const mapStateToProps = state => {
return state.authenticationReducer.loggedUser
}
const AuthorizedRoute = connect(mapStateToProps, { getUser: getLoggedUser })(AuthorizedRouteComponent);
export default AuthorizedRoute
Find Logged user action
export function requestFetch() {
return {
type: FETCH_REQUEST,
pending: true,
logged: false
}
}
export function receiveFetch(userData) {
return {
type: FETCH_SUCCESS,
pending: false,
logged: true,
userData
}
}
export function fetchError(message) {
return {
type: FETCH_FAILURE,
pending: false,
logged: false,
errorMessage:message
}
}
export function getLoggedUser() {
return dispatch => {
dispatch(requestFetch())
return axios.get('/o3/rest/user/userdetails')
.then(response => {
if (!response.status === 200) {
dispatch(fetchError(response.statusText))
} else {
dispatch(receiveFetch(response.data))
}
}
).catch(function (error) {
dispatch(fetchError(error.response.statusText))
}
)
}
}
And finally my Logged user reducer
export function loggedUser(state = initialState, action) {
switch (action.type) {
case FETCH_REQUEST:
case FETCH_SUCCESS:
case FETCH_FAILURE:
let obj = Object.assign({}, state, action);
return obj;
default:
return state
}
}
You can use localStorage to save the user in your action creator that is a charge of fetching the user:
export function getLoggedUser() {
return dispatch => {
dispatch(requestFetch())
return axios.get('/o3/rest/user/userdetails')
.then(response => {
if (!response.status === 200) {
dispatch(fetchError(response.statusText))
} else {
localStorage.setItem('userData',response.data)
}
}
).catch(function (error) {
dispatch(fetchError(error.response.statusText))
}
)
}
}
And then in the in the index file ask:
const userData = localStorage.getItem('userData')
if(userData){
store.dispatch({
type: FETCH_SUCCESS,
pending: false,
logged: true,
userData
})
}
Each time you refresh the page or just type a URL like http://url/app/order is going to verify is there exist a user currently logged, if exist will dispatch an action an update your state.
Related
I'm creating a basic app using React and Express. The issue I'm having is that I can login a user and register a new user, but when I hit the refresh button, it logs the same user out, immediately. In addition, their authorization token is still present in the localStorage even though the user is logged out. I don't think its coming from the server because the server (via Postman) performs all necessary functions as needed. The error has to be coming from the front end. In addition, I had my actions in state set within a try/catch block. I removed the catch for errors block because whenever I would register/login a user, it would perform the behaviors as needed, but the authorization would come back as a 401. So in short, I could register a user, but I couldn't login a user, nor keep that new user logged in. Once I removed the try/catch block and just created a basic async/await function, it would allow me to register/login a user without any issues....except now, I can't stay logged in after refreshing the page. Here is my code.
Register.js
...imports
const Register = () => {
const alertContext = useContext(AlertContext)
const authContext = useContext(AuthContext)
const { setAlert } = alertContext
const { register, error, clearErrors, isAuthenticated, loggedIn, loadUser } = authContext
const [user, setUser] = useState({
name: '',
email: '',
password: '',
password2: '',
})
const [ activeUser, setActiveUser ] = useState()
const { name, email, password, password2 } = user
useEffect(() => {
const loggedInUser = localStorage.getItem('token')
if(loggedIn === true && loggedInUser){
loadUser()
setActiveUser(loggedInUser)
}
if(error === 'User already exists'){
setAlert(error, 'danger')
clearErrors()
}
// eslint-disable-next-line
}, [error])
const onChange = (e) => {
setUser({ ...user, [e.target.name]: e.target.value })
}
const onSubmit = (e) => {
e.preventDefault()
if (name === '' || email === '' || password === '') {
setAlert('Please enter all fields', 'danger')
} else if (password !== password2) {
setAlert('Passwords do not match', 'danger')
} else {
register({
name,
email,
password
})
}
}
if (isAuthenticated) return <Navigate to='/' />
return (
<div className='form-container'>
<h1>
Account <span className='text-primary'>Register</span>
</h1>
<form onSubmit={onSubmit}>
<div className='form-group'>
<label htmlFor='name'>Name</label>
<input type='text' name='name' value={name} onChange={onChange} />
<label htmlFor='email'>Email</label>
<input type='email' name='email' value={email} onChange={onChange} />
<label htmlFor='password'>Password</label>
<input
type='password'
name='password'
value={password}
onChange={onChange}
minLength='6'
/>
<label htmlFor='password2'>Confirm Password</label>
<input
type='password'
name='password2'
value={password2}
onChange={onChange}
minLength='6'
/>
</div>
<button
type='submit'
className='btn btn-primary btn-block'
> Register
</button>
</form>
</div>
)
}
export default Register
AuthState.js
imports..
const AuthState = ({ children }) => {
const intitialState = {
token: localStorage.getItem("token"),
isAuthenticated: null,
loading: true,
error: null,
user: null,
loggedIn: false
}
const [state, dispatch] = useReducer(AuthReducer, intitialState)
//Load User
const loadUser = async () => {
const { token } = state
const config = {
headers: {
'Content-type': 'application/json',
'x-auth-token': token
}
}
const res = await axios.get('/api/auth', config)
dispatch({
type: USER_LOADED,
payload: res.data
})
}
//Login User
const login = async (formData) => {
const config = {
headers: {
'Content-type': 'application/json'
}
}
const res = await axios.post('/api/auth', formData, config)
dispatch({
type: LOGIN_SUCCESS,
payload: res.data
})
loadUser()
}
//Register User
const register = async (formData) => {
const config = {
headers: {
'Content-type': 'application/json',
}
}
const res = await axios.post('/api/users', formData, config)
dispatch({
type: REGISTER_SUCCESS,
payload: res.data
})
}
//Logout
const logout = () => {
localStorage.removeItem('token')
dispatch({
type: LOGOUT
})
}
//Clear Errors
const clearErrors = () => {
dispatch({ type: CLEAR_ERRORS})
}
const { token, isAuthenticated, loading, error, user, loggedIn } = state
return (
<authContext.Provider
value={{
token,
isAuthenticated,
loading,
error,
user,
loggedIn,
register,
clearErrors,
loadUser,
login,
logout
}}
>
{children}
</authContext.Provider>
)
}
export default AuthState
PrivateRoute.js
const PrivateRoute = ({ component: Component }) => {
const authContext = useContext(AuthContext)
const { isAuthenticated, loggedIn } = authContext
if (isAuthenticated || loggedIn) return <Component />;
return <Navigate to='/login' />;
}
Navbar.js (for logging out)
const Navbar = ({ title, icon }) => {
const authContext = useContext(AuthContext)
const { isAuthenticated, logout, user } = authContext
const onLogout = () => {
logout()
}
const authLinks = (
<>
<li> Hello { user && user.user.name }</li>
<li>
<a onClick={onLogout} href="#!">
<i className="fas fa-sign-out-alt" /><span className="hide-sm">Logout</span>
</a>
</li>
</>
)
const guestLinks = (
<>
<li>
<Link to='/register'>Register</Link>
</li>
<li>
<Link to='/login'>Login</Link>
</li>
</>
)
return (
<div className="navbar bg-primary">
<h1>
<i className={icon} /> {title}
</h1>
<ul>
{ isAuthenticated ? authLinks : guestLinks }
</ul>
</div>
)
}
AuthReducer.js
imports..
export default (state, action) => {
switch(action.type) {
case REGISTER_SUCCESS:
localStorage.setItem('token', action.payload.token)
return {
...state,
...action.payload,
isAuthenticated: true,
loading: false,
loggedIn: true
}
//case REGISTER_FAIL:
//case AUTH_ERROR:
//case LOGIN_FAIL:
case LOGOUT:
return {
...state,
token: null,
isAuthenticated: false,
loading: false,
user: null,
error: action.payload,
loggedIn: false
}
case CLEAR_ERRORS:
return {
...state,
error: null
}
case USER_LOADED:
return {
...state,
isAuthenticated: true,
loading: false,
user: action.payload,
loggedIn: true
}
case LOGIN_SUCCESS:
// eslint-disable-next-line
case REGISTER_SUCCESS:
localStorage.setItem('token', action.payload.token)
return {
...state,
...action.payload,
isAuthenticated: true,
loading: false,
loggedIn: true
}
default:
throw Error(`Unhandled type: ${action.type}, ${action.payload}`)
}
}
App.js
...
import AuthState from './components/context/auth/AuthState'
import Register from './components/auth/Register'
import Login from './components/auth/Login'
import PrivateRoute from './components/routing/PrivateRoute'
const App = () => {
return (
<AuthState>
<ContactState>
<AlertState>
<Router>
<div className='App'>
<Navbar />
<div className='container'>
<Alerts />
<Routes>
< Route path='/' element={<PrivateRoute component={Home} />} />
<Route path='/about' element={<About />} />
<Route path='/register' element={<Register />} />
<Route path='/login' element={<Login />} />
</Routes>
</div>
</div>
</Router>
</AlertState>
</ContactState>
</AuthState>
)
}
export default App
here is when my state looks like before I register/login a user.
[
{
"name": "Reducer",
"value": {
"token": null,
"isAuthenticated": null,
"loading": true,
"error": null,
"user": null,
"loggedIn": false
},
"subHooks": [],
"hookSource": {
"lineNumber": 1715,
"functionName": "AuthState",
"fileName": "http://localhost:4445/static/js/bundle.js",
"columnNumber": 78
}
}
]
Here is my state after registering/logging in a user
[
{
"name": "Reducer",
"value": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoiNjI4YmZjZGYyMTE1MDFhMmJmYjViYzU0In0sImlhdCI6MTY1MzM0MTQwOCwiZXhwIjoxNjUzNzAxNDA4fQ.9O6Loe_8gSEXJ5k0xZP-J4f5LtlDYKnSmwmC2HFJTrA",
"isAuthenticated": true,
"loading": false,
"error": null,
"user": "{user: {…}}",
"loggedIn": true
},
"subHooks": [],
"hookSource": {
"lineNumber": 1715,
"functionName": "AuthState",
"fileName": "http://localhost:4445/static/js/bundle.js",
"columnNumber": 78
}
}
]
Upon refreshing the page, this is state
[
{
"name": "Reducer",
"value": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoiNjI4YmZjZGYyMTE1MDFhMmJmYjViYzU0In0sImlhdCI6MTY1MzM0MTQwOCwiZXhwIjoxNjUzNzAxNDA4fQ.9O6Loe_8gSEXJ5k0xZP-J4f5LtlDYKnSmwmC2HFJTrA",
"isAuthenticated": null,
"loading": true,
"error": null,
"user": null,
"loggedIn": false
},
"subHooks": [],
"hookSource": {
"lineNumber": 1715,
"functionName": "AuthState",
"fileName": "http://localhost:4445/static/js/bundle.js",
"columnNumber": 78
}
}
]
In my Applications tab, the token is still present in localStorage, but the user is logged out.
Key: Token
value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoiNjI4YmZjZGYyMTE1MDFhMmJmYjViYzU0In0sImlhdCI6MTY1MzM0MTQwOCwiZXhwIjoxNjUzNzAxNDA4fQ.9O6Loe_8gSEXJ5k0xZP-J4f5LtlDYKnSmwmC2HFJTrA
Any help would be greatly appreciated, I've been working on this for about 4 days and cannot understand exactly where I'm going wrong.
The problem is that when I first Logged-in, The userId Variable on renderList() or the this.props.user will always be null. it will work when I refreshed it. I tried checking it on the first line of renderList function but it seems it will always be null after I logged in. I even tried to dispatch the fetchBlog actions before redirecting after logging in successfully.
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Card, Button } from 'react-bootstrap';
import { fetchBlogs, deleteBlog } from '../../actions';
class FetchBlogs extends React.Component{
componentDidMount(){
this.props.fetchBlogs();
}
renderButtons(blog, userId){
if(blog._id = userId){
return (
<div>
<Button as={Link} to={`/blogs/edit/${blog._id}`} className="mx-2" variant="outline-warning" size="md">Edit</Button>
<Button onClick={() => {
this.props.deleteBlog(blog._id)
}}
className="mx-2"
variant="outline-danger"
size="md">
Delete
</Button>
</div>
);
}
return '';
}
renderList(){
const userId = this.props.user && this.props.user._id;
return this.props.blogs.map(blog => {
return (
<Card className="m-2" key={blog.title}>
<Card.Body>
<Card.Title as={Link} to={`/blogs/${blog._id}`}>{blog.title}</Card.Title>
<Card.Text>{blog.content}</Card.Text>
</Card.Body>
<div className="mb-2">
{this.renderButtons(blog, userId)}
</div>
</Card>
);
})
}
render(){
return (
<div>
<h2 className="m-2">My Blogs</h2>
{this.renderList()}
</div>
);
}
}
const stateToProps = state => {
return {
blogs: Object.values(state.blogs),
user: state.auth.user,
}
}
export default connect(stateToProps, { fetchBlogs, deleteBlog, })(FetchBlogs);
This is the code for my action login
export const login = formValues => async dispatch => {
const config = {
header: {
'Content-Type': 'application/json'
}
};
try{
const res = await portfolios.post('/auth/login', formValues, config)
dispatch({
type: 'LOGIN_SUCCESS',
payload: res.data
});
dispatch(fetchBlogs())
history.push('/blogs/all');
}catch(e){
dispatch({
type: 'LOGIN_FAILED',
});
history.push('/auth/login');
}
}
This is my Auth Reducer
const INITIAL_STATE = {
access: localStorage.getItem('access'),
isAuthenticated: null,
isLoading: false,
user: null,
}
const authReducer = (state = INITIAL_STATE, action) => {
switch(action.type){
case 'LOGIN_SUCCESS':
case 'REGISTER_SUCCESS':
localStorage.setItem("access", action.payload.token);
return {
...state,
access: action.payload.token,
user: action.payload.user,
isAuthenticated: true,
isLoading: false
};
case 'AUTH_ERROR':
case 'LOGIN_FAILED':
case 'LOGOUT_SUCCESS':
case 'REGISTER_FAIL':
localStorage.removeItem('access');
return {
...state,
user: null,
isAuthenticated: false,
isLoading: false,
}
default:
return state;
}
}
I don't know what am I missing. Ive checked and Searched some answers and I did check if the prop is null but still no changes
I'm having some problems with deleting the post in my app. So, after deleting the post, the state should update and the component should re-render, right? So, after deleting my post, component re-renders with the same data. If I refresh, then only the updated data is shown on the page. For example, if I have 3 posts in my app when I delete ONE post, the component re-renders, but still it shows 3 posts. I don't know why this is happening.
Here's my code.
UserFeed
import React, { Component } from "react"
import { getUserPosts, getCurrentUser } from "../actions/userActions"
import { connect } from "react-redux"
import Cards from "./Cards"
class UserFeed extends Component {
componentDidMount() {
const authToken = localStorage.getItem("authToken")
if (authToken) {
this.props.dispatch(getCurrentUser(authToken))
if (this.props && this.props.userId) {
this.props.dispatch(getUserPosts(this.props.userId))
} else {
return null
}
}
}
render() {
console.log("render called")
const { isFetchingUserPosts, userPosts } = this.props
console.log(isFetchingUserPosts, userPosts)
return isFetchingUserPosts ? (
<p>Fetching....</p>
) : (
<div>
{userPosts &&
userPosts.map(post => {
return <Cards key={post._id} post={post} />
})}
</div>
)
}
}
const mapStateToPros = state => {
return {
isFetchingUserPosts: state.userPosts.isFetchingUserPosts,
userPosts: state.userPosts.userPosts.userPosts,
userId: state.auth.user._id
}
}
export default connect(mapStateToPros)(UserFeed)
Cards
import React, { Component } from "react"
import { connect } from "react-redux"
import { deletePost } from "../actions/userActions"
class Cards extends Component {
handleDelete = (_id) => {
this.props.dispatch(deletePost(_id))
}
render() {
const { _id, title, description } = this.props.post
return (
<div className="card">
<div className="card-content">
<div className="media">
<div className="media-left">
<figure className="image is-48x48">
<img
src="https://bulma.io/images/placeholders/96x96.png"
alt="Placeholder image"
/>
</figure>
</div>
<div className="media-content" style={{border: "1px grey"}}>
<p className="title is-5">{title}</p>
<p className="content">{description}</p>
<button onClick={() => {this.handleDelete(_id)}} className="button is-success">Delete</button>
</div>
</div>
</div>
</div>
)
}
}
const mapStateToProps = state => {
return state
}
export default compose(withRouter, connect(mapStateToProps))(Cards)
deletePost action
export const deletePost = (id) => {
return async dispatch => {
dispatch({ type: "DELETING_POST_START" })
try {
const res = await axios.delete(`http://localhost:3000/api/v1/posts/${id}/delete`)
dispatch({
type: "DELETING_POST_SUCCESS",
data: res.data
})
} catch(error) {
dispatch({
type: "DELETING_POST_FAILURE",
data: { error: "Something went wrong" }
})
}
}
}
userPosts reducer
const initialState = {
isFetchingUserPosts: null,
isFetchedUserPosts: null,
userPosts: [],
fetchingUserPostsError: null,
isDeletingPost: false,
isDeletedPost: false,
deletingError: false,
}
const userPosts = (state = initialState, action) => {
switch (action.type) {
case "FETCHING_USER_POSTS_START":
return {
...state,
isFetchingUserPosts: true,
fetchingUserPostsError: null,
}
case "FETCHING_USER_POSTS_SUCCESS":
return {
...state,
isFetchingUserPosts: false,
isFetchedUserPosts: true,
userPosts: action.data,
fetchingUserPostsError: null,
}
case "FETCHING_USER_POSTS_ERROR":
return {
...state,
isFetchingUserPosts: false,
isFetchedUserPosts: false,
fetchingUserPostsError: action.data.error,
}
case "DELETING_POST_START":
return {
...state,
isDeletingPost: true,
deletingError: null,
}
case "DELETING_POST_SUCCESS":
const filteredPostList = state.postList.filter((post) => post._id !== action.data._id)
return {
...state,
isDeletingPost: false,
isDeletedPost: true,
userPosts: filteredPostList,
deletingError: null,
}
case "DELETING_POST_ERROR":
return {
...state,
isDeletingPost: false,
deletingError: action.data.error,
}
default:
return state
}
}
export default userPosts
Delete post action needs to pass on id to the reducer upon success.
Delete post action
export const deletePost = (id) => {
return async dispatch => {
dispatch({ type: "DELETING_POST_START" })
try {
const res = await axios.delete(`http://localhost:3000/api/v1/posts/${id}/delete`)
dispatch({
type: "DELETING_POST_SUCCESS",
data: res.data,
id
})
} catch(error) {
dispatch({
type: "DELETING_POST_FAILURE",
data: { error: "Something went wrong" }
})
}
}
}
Access action.id in user posts reducer
case "DELETING_POST_SUCCESS":
return {
...state,
isDeletingPost: false,
isDeletedPost: true,
userPosts: state.postList.filter(post => post._id !== action.id),
deletingError: null,
}
I am pretty new to React and trying to learn by making small, simple applications. I am making a simple React application which has a Login functionality. I am also using Redux store and Redux-saga. My login flow is:
There is a Login component that takes email and password from the user, and on clicking the login button a POST call is made.
email and password are sent to the server, if they are valid the server returns a token in the response which I save in local storage.
If a token is received, action for Login success is fired. Here I set a flag called success: true.
In my front end I check the value of the success flag, and if success==true then I redirect to another page called Customers
Login Component
import React, { Component } from 'react';
import { connect } from "react-redux";
import { withRouter } from 'react-router-dom';
import { loginRequest } from "../../actions/loginActions";
import './styles.css';
class Login extends Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
error: '',
};
}
dismissError = () => {
this.setState({ error: '' });
}
handleSubmit = (evt) => {
evt.preventDefault();
let { email, password } = this.state;
if (!email) {
return this.setState({ error: 'Username is required' });
}
if (!password) {
return this.setState({ error: 'Password is required' });
}
let data = {
email: email,
password: password
}
this.props.login(data); //dispatches a method which then makes the POST call
//the checking happens before the above function has finished executing
if (this.props.success)
this.props.history.push('/customers');
else
return this.setState({
error: 'Invalid Username/Password'
});
}
handleChange = (evt) => {
this.setState({
[evt.target.name]: evt.target.value
});
}
render() {
let { email, password } = this.state;
return (
<form className="loginForm" onSubmit={this.handleSubmit}
action="/upload">
<h2>Login</h2>
{
this.state.error &&
<h3 className='error' onClick={this.dismissError}>
<button onClick={this.dismissError}>✖</button>
{this.state.error}
</h3>
}
<label className="FormFields label">Email</label>
<input type="email" className="FormFields" name="email"
value={email}
onChange={(event) => this.handleChange(event)} />
<br />
<label className="FormFields label">Password</label>
<input type="password" className="FormFields" name="password"
value={password}
onChange={(event) => this.handleChange(event)} />
<br />
<input type="submit" className="FormFields submit"
value="Login" />
</form>
);
}
}
const mapStateToProps = (state) => {
return {
loading: state.login.loading,
success: state.login.success
}
}
const mapDispatchToProps = (dispatch) => {
return { login: (data) => {dispatch(loginRequest(data))} }
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Login));
Login Saga
import { put, takeEvery, call } from 'redux-saga/effects'
import { LOGIN_REQUEST, LOGIN_PENDING, LOGIN_SUCCESS, LOGIN_FAILURE } from '../actions/loginActions';
export function* login(action) {
const { data } = action.payload;
yield put({ type: LOGIN_PENDING })
let url = 'myserverurl/login'
try {
const response = yield call(fetch, url, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
}
});
let tokenObj = yield response.json();
if (response.status === 200) {
localStorage.setItem('user', tokenObj.token);
yield put({ type: LOGIN_SUCCESS, token: tokenObj.token })
}
}
catch (error) {
yield put({ type: LOGIN_FAILURE, error })
}
}
export function* watchLogin() {
yield takeEvery(LOGIN_REQUEST, login)
}
The login reducer is very simple.
Login Reducer
import { LOGIN_REQUEST, LOGIN_PENDING, LOGIN_SUCCESS, LOGIN_FAILURE } from '../actions/loginActions';
const initState = {
loading: false,
success: false,
error: ''
}
const loginReducer = (state = initState, action) => {
switch (action.type) {
case LOGIN_REQUEST:
return {
...state,
loading: false
}
case LOGIN_PENDING:
return {
...state,
loading: true
}
case LOGIN_SUCCESS:
return {
...state,
success: true,
loading: false
}
case LOGIN_FAILURE:
return {
...state,
loading: false,
success: false,
error: action.error
}
default: return state;
}
}
export default loginReducer;
The statement this.props.login(data) in Login Component dispatches the action which then makes a POST call. I want to wait for the entire flow I mentioned above to complete, before it checks the value of success flag, but that doesn't happen.
In the event of a login, how do I wait till the actions of my login reducer are completed before my front end checks for the success flag?? I read the docs on async/await but I didn't really understand how to use them properly. Can anyone help me with this
You cannot immediately check for this.props.success as you are making an async call, you need to add a check for success props in getDerivedStateFromProps
add getDerivedStateFromProps in your Login component
static getDerivedStateFromProps(nextProps, prevState) {
if(!nextProps.loading){
if(nextProps.success === true) {
nextProps.history.push('/customers');
} else {
return { error: 'Invalid Username/Password' }
}
}
return null
}
remove below code from handleSubmit
if (this.props.success)
this.props.history.push('/customers');
else
return this.setState({
error: 'Invalid Username/Password'
});
I do not know why my this.props are showing undefined on handleSubmit. I have done everything possible. When I log state in mapStateToProps, I see what the action has dispatched, but this.props is not taking it.
This is what I get (https://imgur.com/a/AXixWn9) in logger when I enter the wrong details
I have searched online but no clue. I have debugged this for hours
Login.js
import React from 'react';
import { NavLink } from 'react-router-dom';
import { Redirect } from 'react-router';
import qs from 'qs';
import { connect } from 'react-redux';
import Outer from '../templates/outer';
import history from '../../_helpers/history';
import routes from '../../services/urls';
import apiRequest from '../../services/api';
import loginUser from '../../redux/actions/authentication.action';
import { LOGIN_FAILURE, LOGIN_SUCCESS } from '../../redux/constants/authentication.constants';
import alertActions from '../../redux/actions/alert.actions';
class LoginComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
fields: {},
errors: {},
loginError: '',
submitted: false,
};
const { dispatch } = this.props;
dispatch(alertActions.clear());
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(field, e) {
const { fields } = this.state;
fields[field] = e.target.value;
this.setState({ fields });
}
handleValidation() {
const { fields } = this.state;
const errors = {};
let formIsValid = true;
// Login code
if (!fields.code) {
formIsValid = false;
errors.code = 'This field is required';
}
// Password
if (!fields.password) {
formIsValid = false;
errors.password = 'This field is required';
}
this.setState({ errors });
return formIsValid;
}
handleSubmit(e) {
e.preventDefault();
const { code, password } = this.state.fields;
this.setState({ submitted: true });
if (this.handleValidation()) {
this.setState({ submitted: true });
const payload = {
email: code,
// level: 'SYSTEM',
password,
};
const { dispatch } = this.props;
dispatch(loginUser(payload));
} else {
this.setState({ submitted: false });
}
}
render() {
const { loginError, submitted } = this.state;
return (
<Outer>
<form onSubmit={this.handleSubmit}>
<div className="mt-5" />
{loginError
&& <div className="alert alert-danger">{loginError}</div>
}
<label>Login Code</label>
<input type="text" className="form-control" onChange={this.handleChange.bind(this, 'code')} value={this.state.fields.code || ''} />
<div className="text-danger mt-1 mb-1">{this.state.errors.code}</div>
<br />
<label>Password</label>
<input type="password" className="form-control" onChange={this.handleChange.bind(this, 'password')} value={this.state.fields.password || ''} />
<div className="text-danger mt-1 mb-1">{this.state.errors.password}</div>
<br />
<button
className="btn btn-primary btn-block text-uppercase"
type="submit"
disabled={submitted}
>
{ !submitted ? 'Login to manage my cards' : 'loading...' }
</button>
<div className="row no-gutters mt-4">
<div className="col-md-12">
<NavLink to="/reset-password" className="grey">I have forgotten my password</NavLink>
</div>
</div>
</form>
</Outer>
);
}
}
function mapStateToProps(state) {
console.error(state);
const { auth } = state.authentication;
const { alert } = state.alert;
return { auth, alert };
}
export default connect(mapStateToProps)(LoginComponent);
store.js
export const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware,
loggerMiddleware,
),
);
authentication.reducer.js
function loginReducer(state = {}, action) {
switch (action.type) {
case LOGIN_REQUEST:
return {
user_status: LOGIN_REQUEST,
user_data: action,
};
case LOGIN_SUCCESS:
return {
user_status: LOGIN_SUCCESS,
user_data: action,
};
case LOGIN_FAILURE:
return {
user_status: LOGIN_FAILURE,
user_data: action,
};
default:
return state;
}
}
export default loginReducer;
authentication.action.js
function loginUser(payload) {
function request(user) { return { type: LOGIN_REQUEST, user }; }
function success(response) { return { type: LOGIN_SUCCESS, response }; }
function failure(error) { return { type: LOGIN_FAILURE, error }; }
return (dispatch) => {
// const request = apiRequest(routes.LOGIN, 'POST', qs.stringify(payload));
// const ah = loginUser(qs.stringify(payload));
// console.log(ah);
dispatch(request(payload));
const fetch = apiRequest(routes.LOGIN, 'POST', qs.stringify(payload));
return fetch.then((response) => {
switch (response.status) {
case 400:
dispatch(failure(response));
dispatch(alertActions.error(response.data.description));
break;
case 200:
if (response.data.code === 0) {
localStorage.setItem('qwplx44', JSON.stringify(response.data));
dispatch(success(response.data));
history.push('/dashboard');
break;
}
dispatch(failure(response.data.description));
dispatch(alertActions.error(response.data.description));
break;
default:
return {};
}
}).catch((error) => {
dispatch(failure(error.response.data.message));
dispatch(alertActions.error(error.response.data.message.toString()));
// return false;
});
root-reducer
const rootReducer = combineReducers({
authentication: loginReducer,
alert: alertReducer,
});
export default rootReducer;
You aren't connecting your component correctly. You are trying to destruct keys out of the state that aren't there. I don't see a key called auth on your loginReducer. So this line const { auth } = state.authentication; would return undefined. That is why when logging props auth is undefined.
Instead just pluck what you want from state and you can alias authentication as auth while destructuring :)
const mapStateToProps = ({authentication: auth, alert}) => ({
auth,
alert
})
If you are trying to use the data stored in your reducer after making the request, then you should use the componentDidUpdate lifecycle method
componentDidUpdate(prevProps) {
const { auth: prevAuth } = prevProps
const { auth } = this.props
if (auth.user_status && ((!prevAuth.user_status && auth.user_status) || prevAuth.user_status !== auth.user_status)) {
// TODO handle response from api here
/* ex
if (auth.user_status === LOGIN_FAILURE) {
this.setState({loginFailure: auth.user_data})
}
*/
}
}
Few other things.
You aren't binding handleValidation to the class, but are trying to access state.
You are calling this.setState({ submitted: true }); twice in handleSubmit. Second one is redundant and not needed.
I would refactor your apiRequest to handle the qs.stringify and the error / auth / response status handling, so you dont have to write out that same stuff for every api call :)