I want to create a form to edit a use profile. The form renders data from the user object. I want to use React's useState Hook to hold the state of the form and I want to keep a single object to track changes to the form using an onChange function that handles the changes to the whole user object. Why is this not working?
function Profile() {
const [user, setUser] = useState({});
const [errors, setErrors] = useState({});
useEffect(() => {
axios.get(`/api/v1/users/me`)
.then(res => setUser(res.data.user))
}, [])
const onChange = e => {
user[e.target.name] = e.target.value;
setUser(user)
}
return (
< div >
<form onSubmit={null}>
<div className="form-group">
<label htmlFor={user.name}>Name</label>
<input type="text" name="name"
className={`form-control form-control-lg ${errors.name ? 'is-invalid' : ''}`}
onChange={onChange} placeholder="Fred Flintstone" value={user.name || ''}
/>
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input type="email" name="email"
className={`form-control form-control-lg ${errors.email ? 'is-invalid' : ''}`}
onChange={onChange} placeholder="fred.flintstone#aol.com" value={user.email || ''}
/>
</div>
<div className="form-group">
<label htmlFor="username">Username</label>
<input type="text" name="username"
className={`form-control form-control-lg ${errors.username ? 'is-invalid' : ''}`}
onChange={onChange} placeholder="yabadabadu" value={user.username || ''}
/>
</div>
</form>
<div>
<button type="button" className="btn btn-light btn-sm float-right" onClick={() => console.log("Logout")}>Logout</button>
</div>
</div >
)
}
You're modifying the user object in-place. When you call setUser(user), the form won't re-render because the identity of the user object hasn't changed.
Where you have:
const onChange = e => {
user[e.target.name] = e.target.value;
setUser(user)
}
what you want to have instead is something like:
const onChange = useCallback((event) => {
const {name, value} = event.target;
setUser(oldUser => {
return {
...user,
[name]: value,
};
});
}, [setUser]);
As a general rule of thumb, you usually don't want to modify state objects in-place in React.
You should make a copy of users or it will not trigger a render phase as React performs a shallow comparison with the previous state.
const onChange = ({ target: { name, value } }) => {
setUser(user => ({ ...user, [name]: value }));
};
setState() will always lead to a re-render unless shouldComponentUpdate() returns false. If mutable objects are being used and conditional rendering logic cannot be implemented in shouldComponentUpdate(), calling setState() only when the new state differs from the previous state will avoid unnecessary re-renders.
setState API
Related
I have a parent component that is interested in gathering a list of contacts (name+email):
import { useState } from 'react'
import AddContactFn from './components/AddContactFn'
function App() {
const [contacts, setContacts] = useState([])
const addNewContact = (newContact) => {
setContacts([...contacts, newContact])
}
return (
<div>
<AddContactFn newContact={addNewContact} />
</div>
)
}
export default App
through a child component, which renders a form, with two inputs fields and an 'Add' button:
import React from 'react'
import { useState } from 'react'
export default function AddContactFn({ newContact }) {
const [contact, setContact] = useState({name: ' ', email: ' '})
const addContact = (e) => {
e.preventDefault()
if (contact.name === ' ' || contact.email === ' ') {
return
}
newContact(contact)
}
return (
<div>
<h2>Add Contact</h2>
<form onSubmit={addContact}>
<label>Name</label>
<input
type='text'
name='name'
value={contact.name}
placeholder='Name'
onChange={e => setContact({ name: e.target.value })}
/>
<label>Email</label>
<input
type='text'
name='email'
value={contact.email}
placeholder='Email'
onChange={e => setContact({ email: e.target.value })}
/>
<button type='submit'>Add</button>
</form>
</div>
)
}
I am facing two issues:
Warning: A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component.
with both input fields containing text, every time the Add button is pressed, only the last edited value is returned to the parent (ie. either 'name' or 'email' is returned, not both)
Lets fix one at a time
The warning:
Try adding a check for the input value like value={contact.name || ''}
The OR || operator will check if contact.name is valid, if so use it otherwise ''
This is a side effect linked to the second issue
The state:
Your onChange handler needs to add the previous object properties, currently they are not, so try onChange={e => setContact(prevState, ({...prevState, name: e.target.value }))}
By doing onChange={e => setContact({ name: e.target.value })} you are losing all the properties of the state object, and setting the state to only have a name or email property, that is why you get the warning since the name or email are lost.
Since you have two input fields (name & email) you will have to do the same on both:
<label>Name</label>
<input
type='text'
name='name'
value={contact.name || ''}
placeholder='Name'
onChange={e => setContact(prevState => ({...prevState, name: e.target.value}))}
/>
<label>Email</label>
<input
type='text'
name='email'
value={contact.email || ''}
placeholder='Email'
onChange={e => setContact(prevState => ({...prevState, email: e.target.value}))}
/>
When you use setContact({ name: e.target.value }) (or for the email) you are also setting the other field as undefined. The way I would recommend you did this is:
import React from 'react'
import { useState } from 'react'
export default function AddContactFn({ newContact }) {
const [name, setName] = useState(' ');
const [email, setEmail] = useState(' ');
const addContact = (e) => {
e.preventDefault()
if (name === ' ' || email === ' ') {
return
}
newContact({ name, email })
}
return (
<div>
<h2>Add Contact</h2>
<form onSubmit={addContact}>
<label>Name</label>
<input
type='text'
name='name'
value={name}
placeholder='Name'
onChange={e => setName(e.target.value)}
/>
<label>Email</label>
<input
type='text'
name='email'
value={email}
placeholder='Email'
onChange={e => setEmail(e.target.value)}
/>
<button type='submit'>Add</button>
</form>
</div>
)
}
If you want to keep the state as it currently is then you can just do e.g. setContact({ ...contact, name: e.target.value })
You are using Controlled Input where you bind the input tag value to one of contact state property and the onChange event to an arrow function that will update the contact state.
Initially, the contact state has two properties, which are name, and email.
const [contact, setContact] = useState({name: ' ', email: ' '})
...
return (
<input
value={contact.name}
onChange={e => setContact({ name: e.target.value })}
/>
<input
value={contact.email}
onChange={e => setContact({ email: e.target.value })}
/>
)
When the onChange event occurs, it will update the contact state with a new object that only has one property. Here, React will raise a warning as one of the input values detects the change and found that the binding reference is missing/undefined. At that moment, your input has become an Uncontrolled Input. That's why you got this error:
Warning: A component is changing a controlled input to be
uncontrolled. This is likely caused by the value changing from a
defined to undefined, which should not happen. Decide between using a
controlled or uncontrolled input element for the lifetime of the
component.
To fix it, you can clone the contact by using a shallow copy and update your setter like this:
onChange={e => setContact({ ...contact, name: e.target.value })}
onChange={e => setContact({ ...contact, email: e.target.value })}
So, whenever onChange event occurs, the updated contact state will always have two properties both name, and email. Of course, you can use the other technique, as long as the updated value always has consistent keys and the binding reference remain.
Here is an example:
I have a component called Resume.js in which I define a function called handleChange which takes in an input paramater and based on that input value it changes my global state.
Resume.js:
import React from 'react'
import PersonalInfo from './PersonalInfo'
import { useState } from 'react'
const Resume = () => {
const [resumeStates, setResumeStates] = useState({
step: 1,
firstName: '',
lastName: '',
email: '',
phone: '',
address: '',
linkedIn: '',
jobTitle: '',
city: '',
employer: '',
startDate:'',
endDate: '',
responsibilities: ''
})
const nextStep = () => {
const {step} = resumeStates
setResumeStates({
step: step+1
})
}
const prevStep = () => {
const {step} = resumeStates
setResumeStates({
step: step-1
})
}
const handleChange = input => e => {
setResumeStates({[input]: e.target.value})
}
I am passing this handleChange function as props to a component called PersonalInfo.js as such:
return (
<PersonalInfo nextStep={nextStep} handleChange={handleChange} values={values}/>
)
In PersonalInfo.js I am using that prop as an onChange for my input fields:
import React from 'react'
const PersonalInfo = ({nextStep, handleChange, values}) => {
const continue_step = (e) => {
e.preventDefault()
nextStep()
}
return (
<div>
<div className="container-lg w-50 rounded p-3 mt-5 ">
<h2 className="mb-3">Personal Details</h2>
<form>
<div className="mb-3 row">
<div className="form-group">
<label htmlFor="fname" className="form-label">First Name:</label>
<input type="text" className="form-control" id="fname" value={values.firstName} onChange={() => handleChange('firstName')}/>
<div className={`text-danger fw-bold ${alert ? 'hidden': ''}`}>This is a required field</div>
</div>
</div>
<div className="mb-3 row">
<div className="form-group">
<label htmlFor="lname" className="form-label">Last Name:</label>
<input type="text" className="form-control" id="lname" value={values.lastName} onChange={() => handleChange('lastName')}/>
<div className={`text-danger fw-bold ${alert ? 'hidden': ''}`}>This is a required field</div>
</div>
</div>
But my onChange for my input fields is not working as I cannot type anything in my input as a results my states do not change.
I would appreciate any kind of help...
Thank you for your time and attention.
You're passing something called values to the component for the input values, but I don't see it defined anywhere. It looks like it the values should just be the state object, so pass that:
<PersonalInfo nextStep={nextStep} handleChange={handleChange} values={resumeStates}/>
^-- here --^
Additionally, you're removing all of your state values any time you set one:
setResumeStates({[input]: e.target.value})
Unlike the setState operation in older class-based components, the set operation in the useState hook does not determine the difference for you and only update the new parts. It replaces the state with the new state entirely. Make sure you keep the existing state when updating it:
setResumeStates({ ...resumeStates, [input]: e.target.value })
The same will need to be done for your other state updates as well.
And finally, you're not passing the actual change event to the change handler. Only the title. You can pass the change event to the function returned by the function of the change handler:
onChange={e => handleChange('firstName')(e)}
Or [potentially] more simply:
onChange={handleChange('firstName')}
This might be getting a little confusing though and is likely to result in bugs. (After all, it already has.) Instead of the function within a function, just accept both arguments in the handleChange function:
const handleChange = (input, e) => {
setResumeStates({...resumeStates, [input]: e.target.value})
}
And pass them both:
onChange={e => handleChange('firstName', e)}
As an aside, to help you with design-time validation of what's being passed to what functions, you might try making use of TypeScript in your React development.
Ok, you're doing a funception here.
const handleChange = input => e => {
setResumeStates({[input]: e.target.value})
}
This is a "function" that returns a "function that updates state". What you need is to bind a "function that updates state" to "onChange"
So you can try one of these.
First way, just declare a function that receive input event normally.
const handleChange = e => {
setResumeStates({[e.target.name]: e.target.value})
}
<input
type="text"
name="firstName"
value={values.firstName}
onChange={handleChange}
/>
or if you are feeling func-ky and wanna do func-ception. You can bind onChange with handleChange('firstName').
Note that "handleChange('firstName')" will return a function that accept "e" as parameter.
const handleChange = input => e => {
setResumeStates({[input]: e.target.value})
}
<input
type="text"
value={values.firstName}
onChange={handleChange('firstName')}
/>
I am creating CRUD using ReactJS but I have a problem with the React Forms.
in some cases, I need to set an initial value in my input and I am using a logical operator for this, everything is ok, but when I change the value from the input, the value from the input doesn't change.
My component:
export function Modal(props) {
const [inputsValues, setInputsValues] = useState({
id: '',
nameInput: '',
emailInput: '',
phoneInput: ''
})
const handleInputChange = (event) => {
console.log(event.target.value)
inputsValues[event.target.name] = event.target.value
setInputsValues()
}
const handleFormSubmit = (event) => {
event.preventDefault()
console.log(inputsValues)
}
return (
<div className={`area-modal ${props.isOpen ? 'open' : null}`}>
<div className="modal">
<h1>{props.title}</h1>
<form onSubmit={handleFormSubmit} action="">
<label htmlFor="">Name</label>
<div className="area-input">
<input
type="text"
name="nameInput"
value={inputsValues.nameInput}
onChange={handleInputChange}
/>
</div>
<label htmlFor="">Phone</label>
<div className="area-input">
<input
type="text"
name="phoneInput"
value={props.dataForm ? props.dataForm.phone : ''}
onChange={handleInputChange}
/>
</div>
<label htmlFor="">Email</label>
<div className="area-input">
<input
type="email"
name="emailInput"
value={props.dataForm ? props.dataForm.email : ''}
onChange={handleInputChange}
/>
</div>
<button>{props.buttonText}</button>
</form>
</div>
</div>
)
}
Instead of
inputsValues[event.target.name] = event.target.value;
setInputsValues();
you need to use setInputsValues to update the state with the new value:
const { name, value} = e.target;
setInputsValues({ ...inputsValues, [name]: value });
You need to pass the updated input data into your setInputsValues, for example:
const handleInputChange = (event) => {
console.log(event.target.value)
inputsValues[event.target.name] = event.target.value
setInputsValues({...inputsValues});
}
When working with object-based states like this, it is usually good practice to use the destructured assignment to ensure the state updates.
When you're using reacts useState, you don't need to set the state manually. It handles state for you. Also it's better to use object spread to change the state. So you can change handleInputChange same the bellow:
const handleInputChange = (event) => {
const name = event.target.name;
const value = event.target.value;
setInputsValues({...inputsValues, [name] : value })
}
I had a component using classBased react, and it was working fine, I decided to switch to usestate and it stopepd working. Now the handlechange records the state in a random manner, but it doesnt work properly
My handlechange and the useState
const [state, setState] = useState({
email: "",
password: "",
wrongCombo: false,
serverError: false
})
const handleChange = (evt) => {
const target = evt.target;
const value = target.value;
const name = target.name;
console.log(name)
console.log(value)
setState({
[name]: value
});
}
My handlesubmit (it detects random values in that console.log, icant find the logic, but the log wont get both values as per inputed in the handlechange)
const handleSubmit = (event) => {
event.preventDefault();
const { email, password } = state;
console.log(state)
props.login(email, password, "login").
then(data => {
}).catch((err) => {
if (err == "Error: Request failed with status code 403") {
setState({
wrongCombo: true
}, () => {
})
} else if (err == "Network error") {
setState({
serverError: true
})
}
})
}
And this is my render
<div>
<form>
{state.wrongCombo ? <Alert variant="danger" dismissible onClose={handleDismiss}> Wrong email and password combination </Alert> : null}
{state.serverError ? <Alert variant="danger" dismissible onClose={handleDismiss}> Server Error </Alert> : null}
<div class="form-group">
<label for="exampleInputEmail1">Email address</label>
<input type="email" name="email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" placeholder="Enter email" onChange={handleChange} />
</div>
<div class="form-group">
<label for="exampleInputPassword1">Password</label>
<input type="password" name="password" class="form-control" id="exampleInputPassword1" placeholder="Password" onChange={handleChange} />
</div>
<div className="text-center buttonContainer">
<button type="submit" class="btn btn-primary buttonLogin" onClick={handleSubmit}>Submit</button>
</div>
</form>
</div>
From the docs on useState:
unlike this.setState in a class, updating a state variable always replaces it instead of merging it.
You must replace all values in a useState object when updating them. You are only providing an object to the updater with one of the keys, so only that key will be retained.
A simple pattern for doing this would be to spread the previous state before passing your update:
setState(prev => ({...prev, [newKey]: newVal }));
I am new to react and trying to show update values of an input field. When I press any key, it throws the TypeError: Cannot read property 'name' of null error.
onChange = (e) => {
e.preventDefault();
this.setState((prevState) => ({
profile: {
...prevState.profile,
[e.target.name]: e.target.value, //this is where the error is pointed
},
}));
this is my state and I am setting my state in componentDidUpdate after getting my values from the redux store.
state:
state = {
title: "Create Your Profile",
profile: {},
errors: {},
toggleSocialProfileInput: false,
};
componentDidUpdate:
componentDidUpdate(prevProps) {
if (this.props.errors !== prevProps.errors)
this.setState({ errors: this.props.errors });
if (this.props.profileReducer !== prevProps.profileReducer)
this.setState({ profile: data, title: "Edit Your Profile" });
}
I am using these as input where I am setting my values using profile state
const { profile } = this.state;
<TextField
placeholder="* Profile Handle"
name="handle"
value={profile.handle}
onChange={this.onChange}
error={errors.handle}
info="A unique handle for your profile URL. Your full name, company name, nickname"
/>
<TextField
placeholder="Status"
name="status"
value={profile.status ? profile.status : ""}
onChange={this.onChange}
error={errors.status}
info="What are you upto? Or some quote"
/>
<TextField
placeholder="Location"
name="location"
value={profile.location ? profile.location : ""}
onChange={this.onChange}
error={errors.location}
info="City or City, State (eg. Toronto, ON)"
/>
And if you want to see my TextField component:
const TextField = ({
name,
placeholder,
error,
info,
type,
icon,
disabled,
value,
onChange,
}) => {
const containerClass = icon ? "input-group mb-3" : "form-group";
return (
<div className={containerClass}>
{icon && (
<div className="input-group-prepend">
<span className="input-group-text">
<i className={icon} />
</span>
</div>
)}
<input
type={type}
className={`form-control form-control-lg ${error && "is-invalid"}`}
placeholder={placeholder}
name={name}
value={value}
onChange={onChange}
disabled={disabled}
/>
{info && <small className="form-text text-muted">{info}</small>}
{error && <div className="invalid-feedback">{error}</div>}
</div>
);
};
One way I know is that instead of profile I can declare the names of the textfields in the state but I want to keep the code short and thus want to use the profile object in state.
You are setting state using a callback. So first define e.target in a variable, then use it in your setState callback.
This should work
onChange = (e) => {
e.preventDefault();
const target = e.target;
this.setState((prevState) => ({
profile: {
...prevState.profile,
[target.name]: target.value,
},
}));
With event pooling,
The SyntheticEvent is pooled. This means that the SyntheticEvent object will be reused and all properties will be nullified after the event callback has been invoked. This is for performance reasons. As such, you cannot access the event in an asynchronous way.