I'm making a primitive quiz app with 3 questions so far, all true or false. In my handleContinue method there is a call to push the users input from a radio form into the userAnswers array. It works fine for the first run of handleContinue, after that it throws an error: Uncaught TypeError: this.state.userAnswers.push is not a function(…)
import React from "react"
export default class Questions extends React.Component {
constructor(props) {
super(props)
this.state = {
questionNumber: 1,
userAnswers: [],
value: ''
}
this.handleContinue = this.handleContinue.bind(this)
this.handleChange = this.handleChange.bind(this)
}
//when Continue button is clicked
handleContinue() {
this.setState({
//this push function throws error on 2nd go round
userAnswers: this.state.userAnswers.push(this.state.value),
questionNumber: this.state.questionNumber + 1
//callback function for synchronicity
}, () => {
if (this.state.questionNumber > 3) {
this.props.changeHeader(this.state.userAnswers.toString())
this.props.unMount()
} else {
this.props.changeHeader("Question " + this.state.questionNumber)
}
})
console.log(this.state.userAnswers)
}
handleChange(event) {
this.setState({
value: event.target.value
})
}
render() {
const questions = [
"Blargh?",
"blah blah blah?",
"how many dogs?"
]
return (
<div class="container-fluid text-center">
<h1>{questions[this.state.questionNumber - 1]}</h1>
<div class="radio">
<label class="radio-inline">
<input type="radio" class="form-control" name="trueFalse" value="true"
onChange={this.handleChange}/>True
</label><br/><br/>
<label class="radio-inline">
<input type="radio" class="form-control" name="trueFalse" value="false"
onChange={this.handleChange}/>False
</label>
<hr/>
<button type="button" class="btn btn-primary"
onClick={this.handleContinue}>Continue</button>
</div>
</div>
)
}
}
Do not modify state directly! In general, try to avoid mutation.
Array.prototype.push() mutates the array in-place. So essentially, when you push to an array inside setState, you mutate the original state by using push. And since push returns the new array length instead of the actual array, you're setting this.state.userAnswers to a numerical value, and this is why you're getting Uncaught TypeError: this.state.userAnswers.push is not a function(…) on the second run, because you can't push to a number.
You need to use Array.prototype.concat() instead. It doesn't mutate the original array, and returns a new array with the new concatenated elements. This is what you want to do inside setState. Your code should look something like this:
this.setState({
userAnswers: this.state.userAnswers.concat(this.state.value),
questionNumber: this.state.questionNumber + 1
}
Array.push does not returns the new array. try using
this.state.userAnswers.concat([this.state.value])
this will return new userAnswers array
References: array push and array concat
You should treat the state object as immutable, however you need to re-create the array so its pointing to a new object, set the new item, then reset the state.
handleContinue() {
var newState = this.state.userAnswers.slice();
newState.push(this.state.value);
this.setState({
//this push function throws error on 2nd go round
userAnswers: newState,
questionNumber: this.state.questionNumber + 1
//callback function for synchronicity
}, () => {
if (this.state.questionNumber > 3) {
this.props.changeHeader(this.state.userAnswers.toString())
this.props.unMount()
} else {
this.props.changeHeader("Question " + this.state.questionNumber)
}
})
console.log(this.state.userAnswers)
}
Another alternative to the above solution is to use .concat(), since its returns a new array itself. Its equivalent to creating a new variable but is a much shorter code.
this.setState({
userAnswers: this.state.userAnswers.concat(this.state.value),
questionNumber: this.state.questionNumber + 1
}
The recommended approach in later React versions is to use an updater function when modifying states to prevent race conditions:
this.setState(prevState => ({
userAnswers: [...prevState.userAnswers, this.state.value]
}));
I have found a solution. This shoud work for splice and others too. Lets say that I have a state which is an array of cars:
this.state = {
cars: ['BMW','AUDI','mercedes']
};
this.addState = this.addState.bind(this);
Now, addState is the methnod that i will use to add new items to my array. This should look like this:
addState(){
let arr = this.state.cars;
arr.push('skoda');
this.setState({cars: arr});
}
I have found this solution thanks to duwalanise. All I had to do was to return the new array in order to push new items. I was facing this kind of issue for a lot of time. I will try more functions to see if it really works for all functions that normally won't. If anyone have a better idea how to achieve this with a cleaner code, please feel free to reply to my post.
The correct way to mutate your state when you want to push something to it is to do the following. Let's say we have defined a state as such:
const [state, setState] = useState([])
Now we want to push the following object into the state array. We use the concat method to achieve this operation as such:
let object = {a: '1', b:'2', c:'3'}
Now to push this object into the state array, you do the following:
setState(state => state.concat(object))
You will see that your state is populated with the object.
The reason why concat works but push doesn't is because of the following
Array.prototype.push() adds an element into original array and returns an integer which is its new array length.
Array.prototype.concat() returns a new array with concatenated element without even touching in original array. It's a shallow copy.
Related
I'm attempting to add an object at a specific point in my 'data' array which is this components state. The following isn't working, the array simply gets emptied.
addNewBulletAfterActive = () => {
const array = this.state.data;
const newBulletPoint = {
id: this.state.data.length += 1,
title: 'Click to add'
};
const newData = array.splice(this.state.activeBulletPointId, 0, newBulletPoint);
this.setState({
data: newData
});
}
The idea is that if I have a list of 10 bullet points, the user can click on the 4th bullet point and press enter to add a new bullet point directly after. I've not had any issues adding items to the end of the array but it looks like .splice is causing issues.
I believe this should do what you're after.
function addAfter(array, index, newItem) {
return [
...array.slice(0, index),
newItem,
...array.slice(index)
];
}
This function returns a new array with a new item inserted in the middle. It doesn't mutate your original array and so will play nicely with component's state and Redux.
You can then assign the output from this function to your state.
splice returns spliced items (which is empty since you splice 0 items) and mutates original array.
const newData = array.slice(0); // copy
newData.splice(this.state.activeBulletPointId, 0, newBulletPoint);
this.setState({
data: newData
});
I think this could be an easier and faster method to do this
/*Just plain JS*/
function AddAfter(array, newObject){
array.unshift(newObject);
}
/*In react If updating state*/
var _prev = this.state.your_array; //getting the current value for the state object
var newInfo = {id: 1, more: 'This is a new object'};
_prev.unshift(newInfo);
I think it might be silly question to ask but trust I am new to React . I am trying to push new key and value to array of object but I am not able to do it . Could someone please help me how to achieve my goal. Thanks
Code
this.state= {
cartItems=[
{name:'item1',price:'$23'},
{name:'item2',price:'$26'},
{name:'item3',price:'$24'},
]
I want to add new value like quantity:0 in the end of array of object. Please help me
If you want to update state adding a property to each object, you should just use setState in combination with map. Array.prototype.map allows you to transform each object, like this:
this.setState(state => {
cartItems: state.cartItems.map(cartItem => ({
...cartItem, // Keep all old properties
quantity: 0 // Add quantity
})
})
You should do this only if you're calling setState after the initializer. If you need to modifiy the data right when assigning to this.state, just map the array directly.
Maybe try this
const newState = this.state.cartItems.map(item => {
item.quantity = 0;
return item;
});
this.setState({cartItems: newState});
this.state= {
cartItems:[
{name:'item1',price:'$23'},
{name:'item2',price:'$26'},
{name:'item3',price:'$24'},
]
}
updateCartItems=()=>{
var cartItems = this.state.cartItems.map((item)=>{
return {...item,quantity:0}
})
this.setState({cartItems})
}
this.state = {
myArray = [
{
name:"cat",
expand:false
}
]
}
clickItem(item){
item.expand = true;
this.setState({})
}
this.state.myArray.map((item) =>{
return <div onClick={()=>this.clickItem(item)}>{item.name}</div>
})
In React, i have a simple array of objects,
when i click on one of theses object, i want to change their prop and update the state, what is the proper way of doing this.
i feel like there could be a better way
You need to copy your state, update the copied state and the set the state.
this.state = {
myArray = [
{
name:"cat",
expand:false
}
]
}
clickItem(key){
let items = this.state.myArray;
items[key].expand = true;
this.setState({items})
}
this.state.myArray.map((key, item) =>{
return <div onClick={()=>this.clickItem(key)}>{item.name}</div>
})
Okay, a couple of things.
You're mutating the state directly which is going to fail silently and you're also missing the key prop on your <div.
This is easily resolved though by using the data you have available to you. I don't know whether each name is unique but you can use that as your key. This helps React decide which DOM elements to actually update when state changes.
To update your item in state, you need a way to find it within the state originally, so if name is unique, you can use Array.prototype.find to update it.
clickItem(item) {
const targetIndex = this.state.items.find(stateItem => stateItem.name === item.name)
if (targetIndex === -1)
// Handle not finding the element
const target = this.state.items[targetIndex]
target.expand = !target.expand // Toggle instead of setting so double clicking works as expected.
this.setState({
items: this.state.items.splice(targetIndex, 1, target) // This replaces 1 item in the target array with the new one.
})
}
This will update state and re-render your app. The code is untested but it should work.
I have two functions and when I press each of them I create an array.
this.state = {
food:[],
sports:[],
interest:[],
}
_favoriteFood(foodState){
const food = foodState
this.setState({food:food})
console.log(food)
console.log(this.state.food)
}
_favoriteSports(SportsState){
const sports = SportsState
this.setState({sports:sports})
console.log(sports)
console.log(this.state.sports)
}
render(){
return (
<View>
<FavoriteFood favoriteFood={this._favoriteFood}/>
</View>
<View>
<FavoriteSports favoriteSports={this._favoriteSports}/>
</View>
)}
So for example, I am getting arrays like food:[pizza, hodog] and sports:[basketball, surfing] when I call a method by pressing a button.
My question is when I try to merge two arrays like:
const interest = [...this.state.food, ...this.state.sports]
Its showing undefined because I think I am calling it before the render happens.
Should I make another method to merge arrays?
Any advice or comments would be really helpful. Thanks in advance :)
You problem can happen because React doesn't change the state immediately when you call setState, it may change the state later. If you want to access the state after React applies the change, you should use the second argument of the setState method:
_favoriteFood(food){
this.setState({food}, () => {
const interest = [...this.state.food, ...this.state.sports];
});
}
Reference
The other solution is to use your method argument instead of reading the same value from this.state:
_favoriteFood(food){
this.setState({food});
const interest = [...food, ...this.state.sports];
}
BTW, you should not store const interest = [...this.state.food, ...this.state.sports] in the state, because it can be derived from the other state variables.
Push multi item in array
Merge Two Array
var subject1=['item1','item2']
var subject=['item3','item4','item5'];
subject1.push(...subject)
console.log(subject1);
//output
['item1', 'item2', 'item3', 'item4', 'item5']
I hope you can help.
I can't remember where I got the the snippet of code in the deleteHandler function. It deletes the relevant listdata item from the JSON array and re-renders as expected. I just don't understand what it's doing. Is it specific React syntax? Is it rudimentary stuff that I am oblivious to?
I know the state.listdata.splice(id, 1); line gets the current JSON object, but what does the arrow function do? What is being returned? I'm quite baffled by it.
Any help is much appreciated.
var AppFront = React.createClass({
getInitialState:function(){
return{
listdata: [
{"id":1,"name":"Push Repo","description":"Job No 8790","priority":"Important"},
{"id":2,"name":"Second Note","description":"Job No 823790","priority":"Important"}
]
}
},
deleteHandler: function(e,id){
this.setState(state => {
state.listdata.splice(id, 1);
return {listdata: state.listdata};
});
},
render: function(){
var listDataDOM = this.state.listdata.map((item,index) => {return (<li key={item.id}>
{item.name}
<button onClick={()=>this.deleteHandler(item.id)}>delete</button>
</li>)});
return(
<div>
<h1>To-do List</h1>
<ul>
{listDataDOM}
</ul>
</div>
);
}
});
ReactDOM.render(<AppFront />,document.getElementById("container"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
1) About setState
setState function in React looks something like that :
setState(partialState, callback)
Where partialState may be : object , function or null.
In your particular case you use function, which returns an object of state variables.
setState(function(state){ return {some:data} })
and with arrow func (es6) , the same will look like
setState(state=> { return {some:data} })
in yout particular case arrow func used just for short
2) About splice
In handler, you use JS func splice() to remove element from state's array;
But it is bad practice, because it mutates the state of component.And It will cause bugs, problems and unpredictable behavior. You shouldn't mutate your state!
To avoid that you can copy your array through slice(), because slice returns new array.
var newArray = state.listdata.slice()
newArray.splice(index, 1);
3) About deleteHandler and data structure
deleteHandler doesnt work properly, and works only for first position.And if your data will look like that:
listdata: [
{"id":52,"name":"Push Repo","description":"Job No 8790","priority":"Important"},
{"id":11,"name":"Second Note","description":"Job No 823790","priority":"Important"}
]
It will not work at all
For proper result , you should change deleteHandler to this:
deleteHandler: function(e,id){
//find index of element
var index = this.state.listdata.findIndex(e=>e.id==id);
//copy array
var newAray = this.state.listdata.slice();
//delete element by index
newAray.splice(index, 1);
this.setState({listdata: newAray});
},
and button
<button onClick={e=>this.deleteHandler(e,item.id)}>delete</button>
> JSBIN example
or you can delete by index
deleteHandler: function(e,index){
//copy array
var newAray = this.state.listdata.slice();
//delete element by index
newAray.splice(index, 1);
this.setState({listdata: newAray});
},
<button onClick={e=>this.deleteHandler(e,index)}>delete</button>
> JSBIN example
In your AppFront component you have a state
{
listdata: [
{"id":1,"name":"Push Repo","description":"Job No 8790","priority":"Important"},
{"id":2,"name":"Second Note","description":"Job No 823790","priority":"Important"}
]
}
It represents initial data in your component. Every time you change state, your component gets rerendered.
You can change state by calling component's setState method
In deleteHandler
deleteHandler: function(e,id){
this.setState(state => {
// state.listdata - array of initial values,
state.listdata.splice(id, 1);
return {listdata: state.listdata}; // returns a new state
});
}
state.listdata.splice(id, 1) // removes an element with index == id from the array. You should not confuse listdata item.id and item index. In order for your code to work correctly you need to pass index in you deleteHandler.
<button onClick={()=>this.deleteHandler(index)}>delete</button>
Another thing is that you call deleteHandler only with one argument - item index so in your definition it should be
deleteHandler: function(index){
this.setState(state => {
// state.listdata - array of initial values,
state.listdata.splice(index, 1);
return {listdata: state.listdata}; // returns a new state
});
}
In your render method you iterate through this.state.listdata and return React.DOM nodes for each.
When you update component's state it gets rerendered and you see that item was deleted.
This code is written in es2015 so if it's new to you, it's better to start from reading something about new syntaxis.
state.listdata.splice(id, 1) deletes 1 element with the index equal to id from listdata array. For example if id equals to 0, then, after applying state.listdata.splice(id, 1), state.listdata will become:
listdata: [
{"id":2,"name":"Second Note","description":"Job No 823790","priority":"Important"}
]
And exactly this array will be returned by this arrow functions.
Keeping in mind, that splice method receives index as first argument, but you pass id property there, most probably you should change this code:
<button onClick={()=>this.deleteHandler(item.id)}>delete</button>
To:
<button onClick={()=>this.deleteHandler(index)}>delete</button>