React setState doesn't work as I expected - javascript

I making a form validation on the page. In one of the tutorials I found a validation method that works correctly but I don't understand one thing.
I declare a variable before the class that stores the object with the default input values. When the field is wrong, the corresponding property is modified, while after correctly filling the form the fields are cleared to default values (errors properties becomes empty).
I don't understand the moment when state is set to the default values. Since the initialState object is a reference type, it should have different values after their change in the validate method.
For example, when I assign a string to the usernameError property, I expected this value to remain in this object. Meanwhile, when I do it:
this.setState(initialState);
Form errors will be cleared.
Shortened code:
const initialState = {
username: "",
surname: "",
email: "",
usernameError: "",
surnameError: "",
emailError: "",
};
class Contact extends React.Component {
constructor(props) {
super(props);
this.state = initialState;
this.handleSubmit = this.handleSubmit.bind(this);
}
validate() {
let usernameError = "";
if (!this.state.username) {
usernameError = "Fill in the missing field";
}
if ( usernameError ) {
this.setState({ usernameError });
return false;
}
return true;
}
handleSubmit(e) {
e.preventDefault();
const isValid = this.validate();
if (isValid) {
this.setState(initialState);
}
}
handleChange = (event) => {
this.setState({
[event.target.name]: event.target.value,
});
};
render() {
// contact form...
}

What you see is the correct behavior. You have to think of this.state = initialState as the new copy creation of initialState object. Because if its just the reference to initialState object, react state object will have no power to deal with state properties. So, to make your code working as per expectations, you have to make these objects separate, or create a copy explicitly. To reset errors, update the state as this.setState({usernameError :''});
Or in the other case of initialState the result will be same but you will be confused.

Related

Clear keys within an object in state

I have a component, which is sending as a prop a method to change the forms input value (like any basic form). after having the onChange working, I decided to add a clear button to clear all input values at once, therefore I created a method to clear all inputs.
Example:
class ComponentName extends Component {
constructor(props) {
super(props);
this.state = {
formInputs: {
inputOne: "",
inputTwo: "",
inputThree: ""
}
};
};
handleOnChange = (event) => {
const { target: { name, value } } = event;
this.setState(previousState => {
return { formInputs: { ...previousState.formInputs, [name]: value } };
});
};
clearInputs = () => {
/* this is problematic, because I'm setting the input from defined to undefined */
this.setState({ formInputs: {} });
};
render() {
const { formInputs } = this.state;
return (
<Form
handleOnChange={this.handleOnChange}
clearInputs={this.clearInputs}
formInputs={formInputs}
/>
);
};
};
clearing like the method above is gonna give an error, because I'm removing the keys within state, which the form inputs have their values aimed to. no problem so I thought, I would just add all inputs within setState to equal an empty string like this: this.setState({ formInputs: { inputOne: "", inputTwo: "", inputThree: ""} }); which works. but I have a lot of inputs which have to be cleared, so probably that method is not the most efficient either. that being said, is there a more efficient way to clear each within the "formInputs" object in state?
You can define the initial state in a separate variable outside the component(or inside if you like) and use it not only to initialize the state but also to reset the state to it inside the clearInputs function.

I am setting a property using setState before dispatching an action, but the property is not set in the redux action on dispatch

I have a class component as follows,
class PassengerAddPage extends React.Component {
state = {
passenger: {
flightNumber: "",
firstName: "",
lastName: "",
address: "",
dob: "",
passportNumber: ""
}
};
I will populate all the fields from form data that user enters, except flightNumber, which will be taken from url param, "this.props.match.params.flightNumber". The handleSubmit for the form is as follows,
handleSubmit = event => {
event.preventDefault();
console.log(this.props.match.params.flightNumber); //prints AH001
this.setState(
{
...this.state,
flightNumber: this.props.match.params.flightNumber
},
() => console.log(this.state.passenger) //prints empty
);
this.props.dispatch(passengerActions.addPassenger(this.state.passenger));
}
Even though I set the flightNumber, it will still show as empty in the Redux store (After dispatching the action, object recieved in the action will contain empty value for flightNumber property).
Please help. I am new to ReactJS and JS.
Reasons could be for:
it will still show as empty in the Redux store
1) You are using this.setState for setting state, which sets the local state of the component not global state in the Redux.
2) You are immediately trying to get flightNumber but it won't set as setState is an async call.
Solution for 1):
Only way to change the state in redux store is to dispatch an action with the data you want to change. For example:
handleSubmit = event => {
event.preventDefault();
this.props.dispatch({type: CHANGE_FLIGHT_NUMBER, flightNumber: flightNumber: this.props.match.params.flightNumber})
}
In Reducer:
case CHANGE_FLIGHT_NUMBER:
return {...state, flightNumber: action.flightNumber}
Solution for 2):
Pass callback to setState:
this.setState(
{
passenger: {...this.state.passenger, flightNumber: this.props.match.params.flightNumber}
},
() => {
console.log(this.state.passenger)
}
)
Assumptions made while writing this answer:
1) You are passing state as (state = initialState, action) to your reducer function.
2) In your store there is state called flightNumber exists.
Since setState is async, use the call back syntax of setState
handleSubmit = event => {
event.preventDefault();
this.setState({
...this.state,
flightNumber: this.props.match.params.flightNumber
},
()=>
{
this.props.dispatch(passengerActions.addPassenger(this.state.passenger));
});
}

ReactJS calling function twice inside child component fails to set parent state twice

I'm having an issue where I want to save the data from a particular fieldset with the default values on componentDidMount().
The data saving happens in the parent component, after it is sent up from the child component. However, as React's setState() is asynchronous, it is only saving data from one of the fields. I have outlined a skeleton version of my problem below. Any ideas how I can fix this?
// Parent Component
class Form extends Component {
super(props);
this.manageData = this.manageData.bind(this);
this.state = {
formData: {}
}
}
manageData(data) {
var newObj = {
[data.name]: data.value
}
var currentState = this.state.formData;
var newState = Object.assign({}, currentState, newObj);
this.setState({
formData: newState, // This only sets ONE of the fields from ChildComponent because React delays the setting of state.
)};
render() {
return (
<ChildComponent formValidate={this.manageData} />
)
}
// Child Component
class ChildComponent extends Component {
componentDidMount() {
const fieldA = {
name: 'Phone Number',
value: '123456678'
},
fieldB = {
name: 'Email Address',
value: 'john#example.com'
}
this.props.formValidate(fieldA);
this.props.formValidate(fieldB)
}
render() {
/// Things happen here.
}
}
You're already answering you're own question. React handles state asynchronously and as such you need to make sure you use the current component's state when setState is invoked. Thankfully the team behind React is well-aware of this and have provided an overload for the setState method. I would modify your manageData call to the following:
manageData(data) {
this.setState(prevState => {
const nextState = Object.assign({}, prevState);
nextState.formData[data.name] = data.value;
return nextState;
});
}
This overload for the setState takes a function whose first parameter is the component's current state at the time that the setState method is invoked. Here is the link where they begin discussing this form of the setState method.
https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
Change manageData to this
manageData(data) {
const newObj = {
[data.name]: data.value
};
this.setState(prevState => ({
formData: {
...prevState.formData,
...newObj
}
}));
}

Keep getting a controlled vs uncontrolled react error

I keep getting this error in my console:
warning.js?8a56:36 Warning: LoginForm is changing a controlled input
of type password to be uncontrolled. Input elements should not switch
from controlled to uncontrolled (or vice versa). Decide between using
a controlled or uncontrolled input element for the lifetime of the
component. More info: https://facebook.github.io/react/docs/forms.html#controlled-components
I have had a look at these questions:
How to create a controlled input with empty default in React 15 - I define my state already
React Controlled vs Uncontrolled inputs - My function takes an event and not a password parameter
React - changing an uncontrolled input - I already define the password as empty
Here is the component:
export default class LoginForm extends Component {
constructor(props) {
super(props);
this.state = {
values: {
username: "",
password: ""
}
};
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this._clearInput = this._clearInput.bind(this);
}
onSubmit(event) {
// this.props.attemptLogin
event.preventDefault();
console.log(this.state.values);
let {username, password} = this.state.values;
this.props.attemptLogin(username, password)
.then((userInfo) => {
console.log(userInfo);
LocalStore.setJson('api', userInfo.api);
LocalStore.setJson('user', userInfo.user);
this._clearInput('username' , 'password');
})
.catch((err) => {
console.log("Failed:");
console.log(err);
this._clearInput('password');
});
}
_clearInput(...fields) {
let newValuesState = {};
fields.forEach((field) => {
newValuesState[field] = '';
});
this.setState({values: newValuesState});
}
onChange(event) {
let name = event.target.name;
let value = event.target.value;
this.setState({values: {[name]: value}});
}
render() {
return (
<div>
<form method="post" onSubmit={this.onSubmit}>
<div><input type="text" name="username" onChange={this.onChange} value={this.state.values.username}/></div>
<div><input type="password" name="password" onChange={this.onChange} value={this.state.values.password}/></div>
<div><button type="submit">Login</button></div>
</form>
</div>
);
}
}
LoginForm.propTypes = {
attemptLogin: React.PropTypes.func.isRequired
};
The code does seem to work but this error pops up in the console and so I can't continue I till I get it to stop appearing. Can anyone see what's I did wrong in the component?
Since you are nesting the values in state.values, your onChange function will remove the field not being changed. You could do something like this instead:
onChange(event) {
let name = event.target.name;
let value = event.target.value;
let values = {...this.state.values};
values[name] = value;
this.setState({values}});
}

Prepopulate controlled components

In my attempt to handle an update form, have written the code below. It is a controlled input component, with a corresponding state value. When a change happens on the input component the state value is updated. This means view will always reflect data changes and the other way around. My issue comes when trying to prepopulate the input component with data fetched from the database. My attempt was to define the initial state value in the constructor, to be equal to the passed props, but that did not work. When the component is first rendered it will not contain the passed spirit prop, since it has not yet been fetched. When the component is rendered the second time (because the data is ready) the constructor will not be called. How will I set the initial state when the data is ready and not before?
SpiritsEditContainer
export default createContainer(({params}) => {
const handle = Meteor.subscribe("spirit", params.id);
return {
loading: !handle.ready(),
spirit: Spirits.find(params.id).fetch()[0]
}
}, SpiritsEditPage);
SpiritsEditPage
export default class SpiritsEditPage extends Component {
constructor(props) {
super(props)
this.state = {name: this.props.spirit.name}
}
handleNameChange(event) {
this.setState({name: event.target.value});
}
handleUpdate(event) {
event.preventDefault();
}
render() {
const {name} = this.state;
if (this.props.loading) {
return <div>loading</div>
} else {
return (
<div>
<h1>SpiritsEditPage</h1>
<form onSubmit={this.handleUpdate.bind(this)}>
<Input type="text"
label="Name"
value={name}
onChange={this.handleNameChange.bind(this)}/>
<button>Update</button>
</form>
</div>
)
}
}
}
The constructor code may not work correctly:
constructor(props) {
super(props)
this.state = {name: this.props.spirit.name}
}
Instead check for props.spirit to be available.
this.state = { name: this.props.spirit && this.props.spirit.name }
Add a componentWillReceiveProps:
componentWillReceiveProps(nextProps) {
if (nextProps.spirit !== this.props.spirit) {
this.setState({ name: nextProps.spirit.name });
}
}
The rest of the code looks alright.

Categories

Resources