I am building a prototype that displays a login form. The submit event triggers a lookup from a database. If the lookup fails, I wish to change the form to a) display the error message and b) discard the previous entry for user ID and password.
My reducer changes the state in Redux, but I am not sure how to transfer the data back to the component state.
Here is my form:
import React from 'react';
import { NavLink } from 'react-router-dom';
import { connect } from 'react-redux';
export class LoginForm extends React.Component {
constructor(props) {
super(props);
console.log("Login form props", props);
this.state = {
userName: props.user ? props.user.userName : '',
password: props.user ? props.user.password : '',
error: props.error ? props.error : ''
}
}
onUserNameChange = (event) => {
const userName = event.target.value;
this.setState(() => ({ userName }));
};
onPasswordChange = (event) => {
const password = event.target.value;
this.setState(() => ({ password }));
};
onSubmit = (event) => {
event.preventDefault();
if (!this.state.userName || !this.state.password) {
this.setState(() => ({ error: 'User name and password are required.'}));
} else {
this.setState(() => ({ error: '' }));
this.props.onSubmit({
userName: this.state.userName,
password: this.state.password
})
}
};
render() {
console.log("Login form render() this.state", this.state);
// console.log("Login form render() this.props", this.props);
return (
<div>
{this.props.error && <p>{this.props.error}</p>}
<form onSubmit={this.onSubmit}>
<input
type="text"
placeholder="User name"
autoFocus
value={this.state.userName}
onChange={this.onUserNameChange}
/>
<input
type="password"
placeholder="Password"
value={this.state.password}
onChange={this.onPasswordChange}
/>
<button>Sign In</button>
</form>
<NavLink to="/passwordRecovery" activeClassName="is-active" exact={true}>Forgot Password?</NavLink>
<NavLink to="/newUser" activeClassName="is-active">New User?</NavLink>
</div>
)
}
}
const mapStateToProps = (state) => {
console.log('in LoginForm state.authentication: ', state.authentication);
if (state.authentication.user)
{
return {
error: state.authentication.error,
userName: state.authentication.user.userName,
password: state.authentication.user.password
}
} else {
return {
error: state.authentication.error,
user: state.authentication.user
}
}
}
export default connect(mapStateToProps, undefined)(LoginForm);
Here is the page which displays the form:
import React from 'react';
import { connect } from 'react-redux';
import LoginForm from './LoginForm';
import { login, resetForm } from '../actions/authentication';
export class LoginPage extends React.Component {
onSubmit = (user) => {
console.log('LoginPage onSubmit user: ', user);
console.log('props ', this.props);
this.props.login(user);
if (this.props.user) {
this.props.history.push("/userHome");
}
}
render() {
console.log("LoginPage.render()", this.props)
return (
<div>
<LoginForm
onSubmit={this.onSubmit} error={this.props.error}
/>
</div>
);
}
}
const mapDispatchToProps = (dispatch) => ({
login: (user) => dispatch(login(user)),
resetForm: () => dispatch(resetForm())
});
const mapStateToProps = (state) => {
console.log('state.authentication: ', state.authentication);
return {
error: state.authentication.error,
user: state.authentication.user
};
}
export default connect(mapStateToProps, mapDispatchToProps)(LoginPage);
Here is the reducer:
// reducer for authentication actions
const authenticationReducerDefaultState = {
userName: '',
password: ''
};
export default (state = authenticationReducerDefaultState, action) => {
console.log('in reducer, state: ', state);
console.log('in reducer, action: ', action);
switch (action.type) {
case 'LOGIN_REQUEST':
return {
user: action.user,
error: '',
loggedIn: false,
loggingIn: true
};
case 'LOGIN_SUCCESS':
return {
user: action.user,
error: '',
loggedIn: true,
loggingIn: false
}
case 'LOGIN_FAILURE':
return {
user: authenticationReducerDefaultState,
error: action.error,
loggedIn: false,
loggingIn: false
}
case 'LOGOUT':
return {
user: authenticationReducerDefaultState,
error: '',
loggedIn: false,
loggingIn: false
};
default:
return state;
};
};
Here is the action:
import database from '../firebase/firebase';
const request = (user) => ({
type: 'LOGIN_REQUEST',
user
});
const success = (user) => ({
type: 'LOGIN_SUCCESS',
user
});
const failure = (error) => {
// console.log('failure with error ', error);
return {
type: 'LOGIN_FAILURE',
user: { userName: '', password: '' },
error
}};
export const login = (user) => {
return (dispatch) => {
const { userName, password } = user;
// console.log(`login function for ${userName} password ${password}`);
dispatch(request(user));
let matchedUser = undefined;
return database.ref(`users`).once('value').then((snapshot) => {
snapshot.forEach((childSnapshot) => {
const user = childSnapshot.val();
if (user.userName === userName &&
user.password === password) {
matchedUser = user;
};
});
return matchedUser;
}).then((matchedUser) => {
console.log('matched user', matchedUser);
if (matchedUser) {
dispatch(success(user));
} else {
// console.log('dispatching failure');
dispatch(failure(`An error occurred looking up user ID ${userName}`));
};
console.log('end of login function');
});
}
}
// action generator for logout action
export const logout = () => ({
type: 'LOGOUT'
});
Here is my root reducer:
export default () => {
// Store creation
const store = createStore(
combineReducers({
authentication: authenticationReducer
}),
composeEnhancers(applyMiddleware(thunk))
);
return store;
}
I'm hoping someone has already been down this road. Thanks in advance.
The problem is that even though the props change (redux store is updated), you are using the local state inside LoginForm. You map the values to props only once (LoginForm.constructor).
If you want to react to redux store changes, you need to write some code in order to update the local state if something changes on the store.
static getDerivedStateFromProps (props) {
return {
userName: props.user ? props.user.userName : '',
password: props.user ? props.user.password : '',
error: props.error ? props.error : ''
}
}
Whatever you return in this method will end up updating the local component state.
This kind of scenarios is kind of difficult to maintain. You are mixing the concept of controlled and uncontrolled components. You are getting the initial values from props, mapping those to the local state, then handle the state changes locally (when the input changes) but also reacting to changes on the store.
Tip: If you use default props you don't have to check if this.props.user is available.
static defaultProps = {
user: {
userName: '',
password: '''
},
error: ''
}
Have you tried dropping the logic from your mapStateToProps
if (state.authentication.user)
{
return {
error: state.authentication.error,
userName: state.authentication.user.userName,
password: state.authentication.user.password
}
} else {
return {
error: state.authentication.error,
user: state.authentication.user
}
}
}
to:
return {
error: state.authentication.error,
userName: state.authentication.user.userName,
user: state.authentication.user
password: state.authentication.user.password
}
Related
i am working on createContext and reducers, with reducers i am updating state value, but it is not updating value at a time, i did console.log(user); in onSubmitSave(), but in console log i am not getting its updated state at a time, i can see it works when i clicked on signin again, here i have attached my whole code, can anyone please look into it, and help me to resolve this issue ?
auth.js
import { createContext } from 'react';
const AuthContext = createContext({
user: null,
hasLoginError: false,
login: () => null,
logout: () => null
});
export default AuthContext;
signin.js
// Login button Click
async function onSubmitSave(e) {
e.preventDefault();
const messages = {
'itemUsername.required': 'Username cannot be empty.',
'itemPassword.required': 'Password cannot be empty.',
};
const rules = {
itemUsername: 'required',
itemPassword: 'required|string',
};
validateAll(state, rules, messages)
.then(async () => {
let data = await login(itemUsername, itemPassword);
console.log(data);
console.log("sdsds");
console.log(user);
setUserData(user);
})
.catch(errors => {
const formattedErrors = {
};
errors.forEach(error => formattedErrors[error.field] = error.message);
setState({
...state,
errors: formattedErrors
});
})
}
// End Login
App.js
const reducer = (state, action) => {
switch (action.type) {
case "login": {
const { username, password } = action.payload;
return {
...state,
hasLoginError: false,
user: {
id: 1,
username: USERNAME,
firstName: "Dev",
lastName: "To"
}
};
}
case "logout":
return {
...state,
user: null
};
default:
throw new Error(`Invalid action type: ${action.type}`);
}
};
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 have a file called Login.js in the file I call this.props.onUserLogin and pass the users auth token. I can verify that I am getting the token from the api call via the console log. When I get to the reducer I loose the value from the state somehow. I can verify that with the console log as well. I am trying to figure out why my state.loginToken is empty in the reducer.
Login.js
import React, {Component} from 'react';
import * as actionTypes from '../Store/actions';
import UserInfoButton from '../UserInfoButton/UserInfoButton';
import { connect } from 'react-redux';
const axios = require('axios');
class Login extends Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
};
}
handleChange = (event) => {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({
[name]: value
})
}
handleLogin = (event) => {
axios.post('http://127.0.0.1:8000/api/user/token/', {
email: this.state.email,
password: this.state.password,
}).then( (response) => {
console.log('response: ' + response.data.token);
this.props.onUserLogin(response.data.token);
})
.catch( (error) => {
console.log(error)
})
event.preventDefault();
}
render() {
const isLoggedIn = this.props.loginToken;
let userButton;
if(isLoggedIn) {
userButton = <UserInfoButton/>
} else {
userButton = "";
}
return (
<div>
<form onSubmit={this.handleLogin}>
Email:<br/>
<input type="text" value={this.state.email} onChange={this.handleChange} name="email"/><br/>
Password:<br/>
<input type="password" value={this.state.password} onChange={this.handleChange} name="password"/><br/>
<br/>
<input type="submit" value="Submit"></input>
</form>
<div>{this.props.loginToken}</div>
<br/>
<div>{userButton}</div>
</div>
)
}
}
const mapStateToProps = state => {
return {
loginToken: state.loginToken
};
}
const mapDispatchToProps = dispatch => {
return {
onUserLogin: (userToken) => dispatch( { type: actionTypes.USER_LOGIN_ACTION, loginToken: userToken } ),
};
}
export default connect(mapStateToProps,mapDispatchToProps)(Login);
reducer.js
import * as actionTypes from './actions';
const initialState = {
loginToken: '',
}
const reducer = (state = initialState, action) => {
switch(action.type) {
case actionTypes.USER_LOGIN_ACTION:
console.log('action: ' + state.loginToken);
return {
...state,
loginToken: state.loginToken,
}
default:
return state;
}
};
export default reducer;
Basically a typo inside of your reducer - You're looking for the loginToken inside of state instead of the action.
case actionTypes.USER_LOGIN_ACTION:
console.log('action: ' + action.loginToken); // not state.loginToken
return {
...state,
loginToken: action.loginToken, // not state.loginToken
}
I have been working on authentication with my project. I have a REST api backend that serves JWT tokens. My front end stack is ReactJS, Redux, Axios and Redux Thunk.
My question is why when I submit my form it does not send any credentials?
But when I console log the action and payload on credChange it seems to be correct. Am I not setting the state somewhere?
Also, axios does not catch the 400 Bad Request error.
Here is my code:
AuthActions.js
export const credChange = ({ prop, value }) => {
return {
type: CRED_CHANGE,
payload: { prop, value },
};
};
export const logoutUser = () => {
return (dispatch) => {
dispatch({ type: LOGOUT_USER });
};
};
const loginSuccess = (dispatch, response) => {
dispatch({
type: LOGIN_USER_SUCCESS,
payload: response.data.token,
});
};
const loginError = (dispatch, error) => {
dispatch({
type: LOGIN_USER_ERROR,
payload: error.response.data,
});
};
export const loginUser = ({ empNum, password }) => {
return (dispatch) => {
dispatch({ type: LOGIN_USER });
axios({
method: 'post',
url: 'http://127.0.0.1:8000/profiles_api/jwt/authTK/',
data: {
emp_number: empNum,
password,
},
})
.then(response => loginSuccess(dispatch, response))
.catch(error => loginError(dispatch, error));
};
};
AuthReducer.js
const INITIAL_STATE = {
empNum: '',
password: '',
empNumErr: null,
passwordErr: null,
authTK: null,
loading: false,
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case CRED_CHANGE:
return { ...state, [action.payload.prop]: action.payload.value };
case LOGIN_USER:
return {
...state,
...INITIAL_STATE,
loading: true,
};
case LOGOUT_USER:
return {
...state,
INITIAL_STATE,
};
case LOGIN_USER_SUCCESS:
return {
...state,
...INITIAL_STATE,
authTK: action.payload,
};
case LOGIN_USER_ERROR:
return {
...state,
...INITIAL_STATE,
empNumErr: action.payload.emp_number,
passwordErr: action.payload.password,
};
default:
return state;
}
};
LoginForm.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
credChange,
loginUser,
logoutUser,
} from '../Actions';
class LoginForm extends Component {
constructor() {
super();
this.onFormSubmit = this.onFormSubmit.bind(this);
this.renderEmpNumErr = this.renderEmpNumErr.bind(this);
this.empNumChange = this.empNumChange.bind(this);
this.passwordChange = this.passwordChange.bind(this);
}
onFormSubmit() {
const { empNum, password } = this.props;
this.props.loginUser({ empNum, password });
}
empNumChange(text) {
this.props.credChange({ prop: 'empNum', value: text.target.value });
}
passwordChange(text) {
this.props.credChange({ prop: 'password', value: text.target.value });
}
renderEmpNumErr() {
if (this.props.empNumErr) {
return (
<p>
{this.props.empNumErr}
</p>
);
}
return null;
}
render() {
return (
<div>
<form onSubmit={this.onFormSubmit}>
<label htmlFor="numberLabel">Employee Number</label>
<input
id="numberLabel"
type="password"
value={this.props.empNum}
onChange={this.empNumChange}
/>
<label htmlFor="passLabel">Password</label>
<input
id="passLabel"
type="password"
value={this.props.password}
onChange={this.passwordChange}
/>
<button type="submit">Login</button>
</form>
{this.renderEmpNumErr()}
</div>
);
}
}
const mapStateToProps = ({ counter }) => {
const {
empNum,
password,
loading,
empNumErr,
passwordErr,
authTK,
} = counter;
return {
empNum,
password,
loading,
empNumErr,
passwordErr,
authTK,
};
};
export default connect(mapStateToProps, { credChange, loginUser, logoutUser })(LoginForm);
After Submitting form with credentials
The console says:
POST XHR http://127.0.0.1:8000/profiles_api/jwt/authTK/ [HTTP/1.0 400 Bad Request 5ms]
And the POST request Raw Data is blank, therefore no credentials were sent.
{"emp_number":["This field is required."],"password":["This field is required."]}
EDIT
If there is any other information I can provide please say so but I think this should be sufficient.
Looks like empNum and password aren't getting set in the state. This is because the action object returned by credChange doesn't get dispatched, so the reducer never get called:
// dispatch calls the reducer which updates the state
dispatch(actionCreator())
// returns an action object, doesn't call reducer
actionCreator()
You can dispatch actions automatically by calling a bound action creator:
// calls the reducer, updates the state
const boundActionCreator = () => {dispatch(actionCreator())}
// call boundActionCreator in your component
boundActionCreator()
mapDispatchToProps can be used to define bound action creators (to be passed as props):
const mapDispatchToProps = (dispatch) => {
return {
credChange: ({ prop, value }) => {dispatch(credChange({prop, value})},
loginUser: ({ empNum, password }) => {dispatch(loginUser({empNum, password})},
logoutUser: () => {dispatch(logoutUser()},
}
}
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);
This should solve the state update issue, allowing props that read from state (empNumber, password, etc.) to update as well.
I'm really new to React. I have an axios request in my actions I want my error message to pass on the component I have this code :
import axios from 'axios';
import setAuthorizationToken from '../utils/setAuthorizationToken';
import jwtDecode from 'jwt-decode';
import { SET_CURRENT_USER, BASE_URL } from './types';
const instance = axios.create({
baseURL: BASE_URL
});
export function setCurrentUser(user) {
return {
type: SET_CURRENT_USER,
user
};
}
export function logout() {
return dispatch => {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
setAuthorizationToken(false);
dispatch(setCurrentUser({}));
}
}
export function login(data) {
return dispatch => {
return instance.post('/authenticate', data).then(function(response) {
const token = response.data.accessToken;
const refreshToken = response.data.refreshToken;
localStorage.setItem('accessToken', token);
localStorage.setItem('refreshToken', refreshToken);
setAuthorizationToken(token);
dispatch(setCurrentUser(jwtDecode(token)));
})
.catch(function(error){
console.log('error: ', error.response.data);
});
}
}
Here is my Component:
import React from 'react';
import TextFieldGroup from '../common/TextFieldGroup';
import validateInput from '../../server/validations/login';
import { connect } from 'react-redux';
import { login } from '../../actions/authActions';
class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
username: '',
password: '',
errors: {},
isLoading: false
};
this.onSubmit = this.onSubmit.bind(this);
this.onChange = this.onChange.bind(this);
}
isValid() {
const { errors, isValid } = validateInput(this.state);
if (!isValid) {
this.setState({ errors });
}
return isValid;
}
onSubmit(e) {
e.preventDefault();
if (this.isValid()) {
this.setState({ errors: {}, isLoading: true });
this.props.login(this.state).then(
(res) => this.context.router.push('/'),
(error) => this.setState({ errors: error.response.data , isLoading: false }),
);
}
}
onChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
render() {
const { errors, username, password, isLoading } = this.state;
return (
<form onSubmit={this.onSubmit}>
<h1>Login</h1>
{ errors.message && <div className="alert alert-danger">{errors.message}</div> }
<TextFieldGroup
field="username"
label="Username"
value={username}
error={errors.username}
onChange={this.onChange}
/>
<TextFieldGroup
field="password"
label="Password"
value={password}
error={errors.password}
onChange={this.onChange}
type="password"
/>
<div className="form-group"><button className="btn btn-primary btn-lg" disabled={isLoading}>Login</button></div>
</form>
);
}
}
LoginForm.propTypes = {
login: React.PropTypes.func.isRequired
}
LoginForm.contextTypes = {
router: React.PropTypes.object.isRequired
}
export default connect(null, { login })(LoginForm);
Here is the console.log
error: Object {code: "UNAUTHORIZED", message: "Invalid username or password."}
Currently I don't know to pass my error message to component. I'm really new to React and Redux
First you have to add the initial state on reducer. For example
authReducer.js
const initialState = {
... // another state
errors: {}
}
function yourReducer(state = initialState, action) {
case 'SHOW_ERROR':
return {
...state,
errors: action.message
}
default:
return state
}
On login action dispatch the 'SHOW_ERROR'
authActions.js
export function login(data) {
return dispatch => {
return instance.post('/authenticate', data).then(function(response) {
...
// success
})
.catch(function(error){
// fail
dispatch({ type: 'SHOW_ERROR', message: error.response.data })
});
}
}
Then you need to map redux state to be a props on your component
LoginComponent.js
function mapStateToProps(state) {
return {
you: may.return.another.state.here,
errors: state.yourReducerName.errors
}
}
export default connect(mapStateToProps, { login })(LoginForm);
Finally, you can call errors as a props on your Component
class LoginForm extends React.Component {
...
render() {
const { errors, username, password, isLoading } = this.state;
const { errors } = this.props // errors from redux state
return (
<form onSubmit={this.onSubmit}>
<p>{errors.message}</p>
<h1>Login</h1>
...
<div className="form-group"><button className="btn btn-primary btn-lg" disabled={isLoading}>Login</button></div>
</form>
);
}
}
Don't forget to validate the prop types. Good luck!