Reputation: 37
I am very new to react and have been learning it through tutorials. Now, since react has been promoting functional components using hooks, I have been trying to convert the tutorial's Class-based method to the new react syntax of functional components. Everything in the following code worked out fine, except for when I try to set the state of AddNinja component to the input value.
Following are three components - the master component(works fine), the display component(works fine), and the problem component. There is also the original version of the problem component as taught in the tutorial using classes and that works fine. Only my version of the problem component(AddNinja) doesn't work.
this is the master component(works fine):
import React, { useState } from 'react';
import Ninjas from './Ninjas';
import AddNinja from './AddNinja';
function App() {
const [ninjas, setNinja] = useState([
{ name: 'Ryu', age: 34, belt: 'blackbelt', id: 1 },
{ name: 'Yuu', age: 32, belt: 'yellowbelt', id: 2 },
{ name: 'Lee', age: 31, belt: 'redbelt', id: 3 },
{ name: 'Shawn', age: 34, belt: 'blackbelt', id: 4 },
{ name: 'Mario', age: 34, belt: 'blackbelt', id: 5 },
{ name: 'Rhea', age: 34, belt: 'blackbelt', id: 6 },
])
const addNinja = (ninja) => {
ninja.id = Math.random() * 10;
let addedNinjas = setNinja([...ninjas, ninja]);
return addedNinjas;
}
const deleteNinja = id => {
let remainingNinjas = ninjas.filter(ninja => ninja.id !== id);
console.log(remainingNinjas);
let newArray = setNinja([...remainingNinjas]);
return newArray
}
return (
<div className="App">
<h1>My first React App</h1>
<p>Welcome</p>
<Ninjas deleteNinja={deleteNinja} ninjas={ninjas} />
<AddNinja addNinja={addNinja} />
</div>
)
}
export default App;
this is the display component(in case you need to run the code)(works fine)
import React from 'react';
const Ninjas = ({ninjas, deleteNinja}) => {
const ninjaList = ninjas.map(ninja => {
return ninja.age > 31 ? (
<div className='ninja' key={ninja.id}>
<div>Name: {ninja.name}</div>
<div>Age: {ninja.age}</div>
<div>Belt: {ninja.belt}</div>
<button onClick={() => {deleteNinja(ninja.id)} }>Delete</button>
</div>
) : null
});
return (
<div className='ninja-list'>
{ninjaList}
</div>
)
}
export default Ninjas;
And this is the problem component.. Everything in it turned out fine while converting class-based to functional using hooks except for the handleSubmit function. While typing the input, it logs the values fine, but then returns all values except the last one(belt) as undefined on submit. It's like it forgets the other values.
import React, { useState } from 'react';
const AddNinja = ({ addNinja }) => {
const [newNinjas, setNewNinja] = useState(
{
name: null,
age: null,
belt: null
}
);
const handleChange = e => {
setNewNinja({
[e.target.id]: e.target.value
});
}
const handleSubmit = (e) => {
e.preventDefault();
console.log('submit', newNinjas.age, newNinjas.belt)
addNinja(newNinjas);
// document.querySelector('form').reset();
}
return (
<div>
<form onSubmit={handleSubmit}>
<label htmlFor="name">Name:</label>
<input type="text" id="name" onChange={handleChange} />
<label htmlFor="age">Age:</label>
<input type="text" id="age" onChange={handleChange} />
<label htmlFor="belt">Belt:</label>
<input type="text" id="belt" onChange={handleChange} />
<button>Submit</button>
</form>
</div>
)
}
export default AddNinja;
What am I doing wrong? Am I supposed to use useEffects here? If yes, can you please show me how to do it in the code?
Also, in case you want to take a look at the original class-based component as taught by the tutor, which works completely fine, by the way, here it is:(works fine)
import React, { Component } from 'react';
class AddNinja extends Component{
state = {
name: null,
age: null,
belt: null
}
handleChange = e => {
this.setState({
[e.target.id]: e.target.value
});
// console.log(this.state)
}
handleSubmit = e => {
e.preventDefault();
// console.log(this.state);
this.props.addNinja(this.state);
document.querySelector('form').reset();
}
render(){
return (
<div>
<form onSubmit={this.handleSubmit}>
<label htmlFor="name">Name:</label>
<input type="text" id="name" onChange={this.handleChange}/>
<label htmlFor="name">Age:</label>
<input type="text" id="age" onChange={this.handleChange}/>
<label htmlFor="name">Belt:</label>
<input type="text" id="belt" onChange={this.handleChange}/>
<button>Submit</button>
</form>
</div>
)
}
}
export default AddNinja;
Thank you so much for reading!
Upvotes: 3
Views: 1676
Reputation: 1074098
The big difference between the setter you get from useState
and the class component setState
method is that the setter from useState
doesn't merge like setState
does. So this:
setNewNinja({
[e.target.id]: e.target.value
});
completely overwrites the existing value in newNinjas
.
Instead, merge your new ninja into the existing object:
setNewNinja({
...newNinjas,
[e.target.id]: e.target.value
});
That version is fine provided you're not dealing with stale state in newNinjas
. Mostly that'll be the case, but sometimes you want to not worry about whether newNinjas
is stale (see this article by Dan Abramov for more on that). In those situations, use the callback version:
setNewNinja(current => ({
...current,
[e.target.id]: e.target.value
}));
There's a note about this in the documentation for useState
:
Note
Unlike the
setState
method found in class components,useState
does not automatically merge update objects. You can replicate this behavior by combining the function updater form with object spread syntax:setState(prevState => { // Object.assign would also work return {...prevState, ...updatedValues}; });
Another option is
useReducer
, which is more suited for managing state objects that contain multiple sub-values.
Upvotes: 3