Reputation: 11
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.
Upvotes: 1
Views: 1480
Reputation: 1079
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
}
Upvotes: 0
Reputation: 495
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: ''
}
Upvotes: 1