State only getting the first letter from the input? - reactjs

Hey I have an issue where I am collecting input from several inputs.. I tried to write a helper function for just updating the state object for each input, but it seems like the input is only gathering the first letter from my input and nothing else.. and it seems like it stores it once and then I can change it.. any idea what I'm missing her?
`
export const Form = ({addStudent}) => {
const [newStudent, setNewStudent] = useState({school:"university"})
const updateValue = e => {
const { name, value } = e.target;
setNewStudent({[name]: value, ...newStudent});
}
return (
<section>
<p>First Name</p>
<input type="text" name="firstName"
onChange={updateValue}
/>
<p>Last Name</p>
<input type="text" name="lastName"
onChange={updateValue}
/>
<label>Choose a school:</label>
<select name="school"
onChange={updateValue}
>
<option value="university">university</option>
<option value="highSchool">High School</option>
</select>
<button onClick={() => addStudent(newStudent)}>
Add new Student
</button>
</section>
)
}
`
I tried to make the updateValue function dynamic with the values like this and now it seems to not work anymore...

You are overwriting the current existing value with your spread.
setNewStudent({[name]: value, ...newStudent});
// ^^^^^^^^^^ overwriting existing value
After the first keystroke (onChange) the field gets created and the value of that field is just one letter (because if was empty before). And it stays in this state because the new value is consistently overwritten by the previous (first) value.
Note: You should be also using a callback while setting the new state, just to make sure it's up-to-date when updating.
setNewStudent((prev) => ({ ...prev, [name]: value }));

you are setting the values ​​in the wrong place.
first copy the state, and the add it the new field.
setNewStudent({...newStudent, [name]: value});

Related

React button disabled property not updating

I have trouble using the disabled property on a button with react. The property works if I hard code a value in the input but the button is not re-rendered automatically when the state is updated through an input change.
export default function App() {
const [user, setUser] = useState({ email: "" });
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<form>
<div>
<div>
<label htmlFor="email">
Email
</label>
</div>
<input
type="text"
name="email"
id="email"
placeholder=""
onChange={(e) =>
setUser((previous) => {
previous.email = e.target.value;
return previous;
})
}
/>
</div>
<button type="submit" disabled={user.email === ""}>
Button
</button>
</form>
</div>
);
}
The problem can be experienced live here:
https://codesandbox.io/s/suspicious-kapitsa-z2zj3m?file=/src/App.js
In the mentioned code previous points to the same reference as the new state you are setting. For complex state variables like objects React finds diff of object using shallow comparison and since both variables point to the object there is no rerender.
Creating a copy of the object which points to a different address in memory, and setting the state using that should fix it.
setUser((previous) => {
const newValue = { ...previous };
newValue.email = e.target.value;
return newValue;
})
Link
That is why it is never a good idea to mutate state in React. Always create a new copy and make changes to that. This is the base of pure functions and functional programming.
Just use:
(e) => setUser((previous) => ({...previous, email: e.target.value}))
You trying to mutate the previous state manually is what prevents React from detecting a change (because the reference stays the same)
Also, to use control forms properly, you need to add this prop to your input:
value={user.email}

React input field mirrors the data from previous input

I am trying to build a small recipe app. One feature of the app is saving user recipes, including ingredient/qty/measurement.
I need to wrap up the inputted ingredients into an array of objects to send to server but right now my setIngredientList only works for the first two ingredients a user inputs.
When a user tries to add a third ingredient it just mirrors the data from the second input (and fills the third input's fields with the same data as the second input). It is like the second inputs and any subsequent input mirror each other.
I believe the problem is init is not clearing properly (it seems to clear after the first ingredient is added allowing the second one to be added, but then it does not clear for the next ingredients.
I'm not sure the proper way to make sure this happens so multiple ingredients can be added.
Here is my code:
const init = {
ingredient_name: '',
quantity: '',
measure: '',
}
export default function Recipe() {
const [name, setName] = useState('')
const [ingredientList, setIngredientList] = useState([
{
ingredient_name: '',
quantity: '',
measure: '',
},
])
const handleChange = (e, i) => {
const { name, value } = e.target
setIngredientList((prevState) => {
const newIngredientList = [...prevState]
newIngredientList[i][name] = value
return [...newIngredientList]
})
}
return (
<div>
<div className="recipe-form-container">
<form className="recipe-form">
[...]
</div>
{ingredientList.map((list, i) => (
<div key={i} className="ingredient-triad">
<input
className="ingredient"
name="ingredient_name"
type="text"
value={list.ingredient_name}
onChange={(e) => handleChange(e, i)}
></input>
<input
className="quantity"
name="quantity"
type="text"
value={list.quantity}
onChange={(e) => handleChange(e, i)}
></input>
<select
className="dropdown"
name="measure"
id="measure"
value={list.measure}
onChange={(e) => handleChange(e, i)}
>
<option value="" disabled>
--none--
</option>
<option value="cup">cup</option>
</select>
<button
onClick={(e) => {
console.log(init)
setIngredientList((prev) => [...prev, init])
e.preventDefault()
}}
>
Add
</button>
</div>
))}
</form>
</div>
</div>
)
}
Classical object reference issue.
Use the below code it will work fine.
Previously, you pass the same init object for multiple rows,
which is why you got that result. Instead of doing that, when the user clicks 'add' button then add a new Object to your state which is derived from your init object. Here I just clone the init object and then set the state.
<button
onClick={(e) => {
console.log(init);
setIngredientList((prev) => [...prev, { ...init }]);
e.preventDefault();
}}
>
Sounds like you wanted some advice and can prob find the solution yourself, but what i would do to this code:
Move e.preventDefault() above setIngredientList.
Create an Ingredient class with the state / logic within and pass state to Ingredient, the list should not not be concerned with an Ingredient, it just lists.
Then what is onClick doing - init - its the initial value isn't it, so I think that with your onClick you are overwriting prev with the value that is outside the component - should it be outside or is it some state - it holds a value - so it should be an initial state of an ingredient? Checkout react tools profiler so see exactly what is happening on your events.
So what is prev we are over writing it, but what is it? Isn't the first arg on onClick the click event? So I think you are overwriting the click event with the init state - which is outside the component - a few things to tweak for you (if you like), hope it helps.

React final form - Unable to pass function to oChange prop

I have a in a form using react final form. I'm trying to pass the value of the current input to a function thats used when the field value changes. However when I type, it's taking no input.
<Field name="myField">
{({ input, meta }) => (
<div>
<TextField
type="text"
name={input.name}
value={input.value}
onChange={() =>
handleChange(input.value)
}
label="Title"
/>
</div>
)}
</Field>
I have a function handleChange above the render method. All I want to do is console the current value, and use a setState hook to update a state variable.
function handleChange(item: string) {
setTitle(item);
console.log(item);
}
What am I doing wrong?
I would personally use a useEffect to achieve this, but the issue is that once you add your own useEffect onto it, then it overrides the default action which saves the form value to the form itself.
So first you still have to do the default onChange. And you want to take the value from the input field using e.target.value.
In your example, you are passing the value of the input field into your handeChange, but becuase you arent saving the value back into the input and the form, its always going to be empty.
<Field name="myField">
{({input}) => (
<div>
<TextField
name={input.name}
value={input.value}
onChange={(e, val) => {
console.log(e.target.value);
handleChange(e.target.value)
input.onChange(e.target.value);
}}
/>
</div>
)}
</Field>

ReactJS: add a new input field on an option select

I'm using react-select with isMulti. I am trying to add a new input field when user selects an option from dropdown. I have somehow achieved it. I am facing a problem, let's say if I select 1st option from dropdown then it adds the input field for that particular option and when I try to select second option then 2 new input fields are added (1 for the previously selected option and the other for current selected option). In this way, I get 3 new input fields but I should be getting 2 input fields because I have selected only 2 options. Below is my code.
onChange = (e) => {
if(e){
e.map((item) => {
this.state.selectValue.push(item.value);
})
}
}
this.state.selectValue.map((item) => {
return(
<div className="row m-3">
<label className="col-md-4">{item}</label>
<div className="col-md-7">
<input type="text" name={item} className="form-control" />
</div>
</div>
)
})
<Select
isMulti
options={this.state.options}
onChange = {(e) => this.onChange(e)}
classNamePrefix="select"
/>
Note: I am storing the option's value as a "name" of input field to keep track of the quantity.
In the screenshot when I selected first then an input field with "first" label was displayed but when I selected "second" then 2 input fields showed up as highlighed in the image.
Updating state within a loop causes sideffects with the way the component re-renders. Try using a temporary array in your handler and then override state after your map function.
onChange = (e) => {
if(e){
let arr = []
e.map((item) => {
arr.push(item.value);
})
this.setState({selectValue: arr})
}
}
Updating state within a loop causes sideffects with the way the component re-renders. Try using a temporary array in your handler and then override state after your map function.

How to clear uncontrolled field in react

I used to use ref for forms but now I always state for forms, I'm facing an issue where I have to clear a field after user submitted something.
handleSumbit = (e) => {
e.preventDefault()
const todoText = this.state.todoText
if(todoText.length > 0){
this.refs.todoTextElem = "" // wont work
this.props.onAddTodo(todoText)
} else {
this.refs.todoTextElem.focus() //worked
}
}
render() {
return(
<div>
<form onSubmit={this.handleSumbit}>
<input ref="todoTextElem" type="text" onChange={e => this.setState({todoText: e.target.value})} name="todoText" placeholder="What do you need to do?" />
<button className="button expanded">Add Todo</button>
</form>
</div>
)
}
Clearing the ref simply don't work because it's a controlled input. I don't want to do something stupid like
passing a flag from parent component telling the form is submitted then use setState to clear the input. Or make onAddTodo to have a callback so that I can do
this.props.onAddTodo(todoText).then(()=>this.state({todoText:""}))
The way you are using the input element is uncontrolled, because you are not using the value property, means not controlling it's value. Simply storing the value in state variable.
You don't need to store the input field value in state variable if you are using ref, ref will have the reference of DOM element, so you need to use this.refName.value to access the value of that element.
Steps:
1- Write the input element like this:
<input
ref= {el => this.todoTextElem = el}
type="text"
placeholder="What do you need to do?" />
To get it's value: this.todoTextElem.value
2- To clear the uncontrolled input field, clear it's value using ref:
this.todoTextElem.value = '';
Write it like this:
handleSumbit = (e) => {
e.preventDefault()
const todoText = this.todoTextElem.value;
if(todoText.length > 0){
this.todoTextElem.value = ''; //here
this.props.onAddTodo(todoText)
} else {
this.todoTextElem.focus()
}
}
Another change is about the string refs, As per DOC:
If you worked with React before, you might be familiar with an older
API where the ref attribute is a string, like "textInput", and the DOM
node is accessed as this.refs.textInput. We advise against it because
string refs have some issues, are considered legacy, and are likely to
be removed in one of the future releases. If you're currently using
this.refs.textInput to access refs, we recommend the callback pattern
instead.
Try and use functional refs instead. Note that the ref is to a DOM element, meaning you still need to address its properties (.value) to modify them as opposed to trying to overwriting the element directly.
The following should work:
handleSumbit = (e) => {
e.preventDefault()
const todoText = this.state.todoText
if(todoText.length > 0){
this.todoTextElem.value = ""
this.props.onAddTodo(todoText)
} else {
this.todoTextElem.focus()
}
}
render() {
return(
<div>
<form onSubmit={this.handleSumbit}>
<input ref={input => this.todoTextElem = input} type="text" onChange={e => this.setState({todoText: e.target.value})} name="todoText" placeholder="What do you need to do?" />
<button className="button expanded">Add Todo</button>
</form>
</div>
)
}

Resources