I am trying to push new items into State array of objects, facing some problem. Below is my code. I am sure I am going something wrong
constructor(props) {
super(props);
this.state = {
bill: []
};
}
componentWillReceiveProps(nextProps, nextState) {
if (nextProps.bills !== this.props.bills) {
let billsObj = nextProps.bills
billsObj.map((billsObj) => {
var joined = this.state.bill.concat({billId:billsObj.id,checked:false});
console.log(joined, "joined", this.state.bill)
this.setState({
bill: [...this.state.bill, ...joined]
}, () => {
console.log(this.state.bill, billsObj.id)
})
})
}
}
In componentWillReceiverProps I am getting the array and then mapping it to push values into state array, But in the end I am only getting a single value in the array , but props array has 11 values and I am only getting single value in my state array. Hope to get some help.
You need to account for previous state if you are updating a piece of state that is derived from the current state, which is explained in detail here. This is why your multiple calls to setState just end up with the last bill in your state array.
It will work as expected if you keep your bills in an intermediary array, and just setState once when you are done:
componentWillReceiveProps(nextProps, nextState) {
if (nextProps.bills !== this.props.bills) {
const bill = nextProps.bills.map(bill => {
return { billId: bill.id, checked: false };
});
this.setState({ bill });
}
}
Related
I have a react hooks function that has a state object apiDATA. In this state I store an object of structure:
{
name : "MainData", description: "MainData description", id: 6, items: [
{key: "key-1", name : "Frontend-Test", description: "Only used for front end testing", values: ["awd","asd","xad","asdf", "awdr"]},
{key: "key-2", name : "name-2", description: "qleqle", values: ["bbb","aaa","sss","ccc"]},
...
]
}
My front end displays the main data form the object as the headers and then I map each item in items. For each of these items I need to display the valuesand make them editable. I attached a picture below.
Now as you can see I have a plus button that I use to add new values. I'm using a modal for that and when I call the function to update state it does it fine and re-renders properly. Now for each of the words in the valuesI have that chip with the delete button on their side. And the delete function for that button is as follows:
const deleteItemFromConfig = (word, item) => {
const index = apiDATA.items.findIndex((x) => x.key === item.key);
let newValues = item.value.filter((keyWord) => keyWord !== word);
item.value = [...newValues];
api.updateConfig(item).then((res) => {
if (res.result.status === 200) {
let apiDataItems = [...apiDATA.items];
apiDataItems.splice(index, 1);
apiDataItems.splice(index, 0, item);
apiDATA.items = [...apiDataItems];
setApiDATA(apiDATA);
}
});
};
Unfortunately this function does not re-render when I update state. And it only re-renders when I update some other state. I know the code is a bit crappy but I tried a few things to make it re-render and I can't get around it. I know it has something to do with React not seeing this as a proper update so it doesn't re-render but I have no idea why.
It is not updating because you are changing the array items inside apiDATA, and React only re-render if the pointer to apiDATA changes. React does not compare all items inside the apiDATA.
You have to create a new apiDATA to make React updates.
Try this:
if (res.result.status === 200) {
let apiDataItems = [...apiDATA.items];
apiDataItems.splice(index, 1);
apiDataItems.splice(index, 0, item);
setApiDATA(prevState => {
return {
...prevState,
items: apiDataItems
}
});
}
Using splice isn't a good idea, since it mutates the arrays in place and even if you create a copy via let apiDataItems = [...apiDATA.items];, it's still a shallow copy that has original reference to the nested values.
One of the options is to update your data with map:
const deleteItemFromConfig = (word, item) => {
api.updateConfig(item).then((res) => {
if (res.result.status === 200) {
const items = apiDATA.items.map(it => {
if (it.key === item.key) {
return {
...item,
values: item.value.filter((keyWord) => keyWord !== word)
}
}
return item;
})
setApiDATA(apiData => ({...apiData, items});
}
});
}
i am building a simple React app where i collect data from a Firebase realtime database and push it as an array (memberArr) into the component state via setState:
this.setState({members: memberArr})
When I log the state in the render() method, I see the contents of the array. In the React Dev Tools the state is also filled as expected.
If I now want to access the contents of the array (e.g. with this.state.members[0]) the console returns undefined.
I initialize the state like this:
constructor(props) {
super(props);
this.state = {
members: [],
};
}
My whole componentDidMount() method looks like this:
componentDidMount() {
const membersRef = firebase.database().ref(`${groupName}/members`);
membersRef.on('value', (data) => {
const memberArr = [];
data.forEach(function(snapshot){
//Gets name of member
var memberName = snapshot.val().name;
//Gets UID of member
var memberKey = snapshot.key;
//Get expenses from fetched member. When ready call function to push everything as array into component state.
this.getExpensesFromUser(memberKey).then(function(data) {
pushArray(data)
});
function pushArray(memberExpense) {
memberArr.push([memberName, memberKey, memberExpense])
};
//This works:
//memberArr.push([memberName, memberKey])
}.bind(this));
this.setState({members: memberArr})
});
}
On a side note: If I avoid calling
this.getExpensesFromUser(memberKey).then(function(data) {
pushArray(data)
});
and only use the following to push the name and key of the members:
memberArr.push([memberName, memberKey])
everything works as expected. console.log(this.state.members[0][0]) returns the name of the first member after the render() method gets triggered due to setState.
Do you have any tips for me?
As Dragos commented, your getExpensesFromUser function returns asynchronous results, so you need to make sure to only call setState once those calls have finished.
const membersRef = firebase.database().ref(`${groupName}/members`);
membersRef.on('value', (data) => {
const promises = [];
data.forEach((snapshot) => {
var memberKey = snapshot.key;
promises.push(this.getExpensesFromUser(memberKey))
});
Promise.all(promises).then((data) => {
const memberArr = data.map((item) => [memberName, memberKey, memberExpense]);
this.setState({members: memberArr})
});
});
So I have a database set up and I am trying to pull the info from it and store it into a state array so I can use it on the website.
This is an example of what I get when I request form the database, shortened for readability
{
"count": 10,
"next": null,
"previous": null,
"results": [
{
"var1": "Test",
"var2": "",
"NeededInfo": "name1",
},
{
"var1": "Test",
"var2": "",
"NeededInfo": "name2",
}
]
}
So what I need is to take the NeededInfo of each object and store it into an array so I can use it on render(). This is what I'm trying, but when I do printToConsole() (not shown but just a console.log(this.state)) to see what the state array looks like, the "other" array is undefined
const title_URL = "http://123.456.789/namelist";
class Project extends Component {
constructor(props) {
super(props);
this.state = {
random1: "",
random2: "",
other: [],
items: []
}
}
componentDidMount() {
fetch(title_URL)
.then(response => {
return response.json();
})
.then(data => {
this.setState({
other: data.results.NeededInfo
})
});
}
I need the "other" array to look like
other: ["name1", "name2"]
because in my render() I have to list the names that the site pulls from the database so the user can see the names of the listed items and do some other stuff with it, and then I can save that into the "items" array, in a new order, but that part works with if I have a preset "other" array.
You need to get NeededInfo from your result and push it into a temporary array (store NeededInfo values only), then just set it into state. For example:
componentDidMount() {
fetch(title_URL)
.then(response => {
return response.json();
})
.then(data => {
// Here you need to use an temporary array to store NeededInfo only
let tmpArray = []
for (var i = 0; i < data.results.length; i++) {
tmpArray.push(data.results[i].NeededInfo)
}
this.setState({
other: tmpArray
})
});
}
Use map and extract the wanted property:
other: data.results.map(({ NeededInfo }) => NeededInfo)
Results is an array of documents.
You cannot access the property of documents inside that array without telling JavaScript that where is that documents inside the array ( index of needed document ).
Create for loop , use I start from 0 , to length of results , and you will be able to use that documents property.
I have an array of 6 objects which have a uid and nothing else. This is so I can repeat over them and have some placeholder content until an object is ready to be added into the array. I set a unique key when a new object is selected. However if I select the same object twice, even though I'm setting a unique key. It seems to update the unique key on the duplicate item (even though the unique key is different).
Might be easier to see the code/app in action here, an example of the problem would be clicking squirtle then blastoise, take a note of the uid's shown. Then click squirtle again and for some reason it updates the old squirtle with the new squirtles uid causing a duplicate key error. https://codesandbox.io/s/l75m9z1xwq or see code below. Math.random is just placeholder until I can get this working correctly.
const initState = {
party: [
{ uid: 0 },
{ uid: 1 },
{ uid: 2 },
{ uid: 3 },
{ uid: 4 },
{ uid: 5 }
]
};
When I click on something this is triggered:
handleClick = pokemon => {
// setup a uid, will need a better method than math.random later
pokemon.uid = Math.random();
this.props.addToParty(pokemon);
};
This then calls a dispatch which triggers the following reducer. Which essentially just checks if the object has no normal ID then replace the content with the payload sent over. It does this but also somehow updates any previous objects with the same uid even though the if statement does not run against them.
const rootReducer = (state = initState, action) => {
if (action.type === "ADD_POKEMON") {
let foundFirstEmptyPoke = false;
const newArray = state.party.map((pokemon, index) => {
if (typeof pokemon.id === "undefined" && foundFirstEmptyPoke === false) {
foundFirstEmptyPoke = true;
pokemon = action.payload; // set the data to the first object that ios empty
}
// if we get to the last pokemon and it's not empty
if (index === 5 && foundFirstEmptyPoke === false) {
pokemon = action.payload; // replace the last pokemon with the new one
}
return pokemon;
});
return {
party: newArray
};
}
return state;
};
The problem here is that, when you click to select a pokemon, you mutate the data you retrieved from the API:
handleClick = pokemon => {
pokemon.uid = Math.random(); // HERE
this.props.addToParty(pokemon);
};
You actually mutate the react state. What you should do is clone your pokemon data object, add an uid to the clone you just generated and update your redux state with it:
handleClick = pokemon => {
this.props.addToParty({
...pokemon,
uid: Math.random()
});
};
That way, no references to the actual react state are kept. Because that was what was happening when you say it updates the old squirtle with the new squirtles uid. When you tried to add another pokemon, you updated the data you retrieved from your API which was also referenced from your first pokemon slot (from your redux state).
In react/redux it's always better to not mutate objects:
this.props.addToParty({...pokemon, uid: Math.random()});
You are mutating the state. Use spread syntax *** to copy the state before updating.
return {
...state,
party: newArray
}
I have a todo list and want to set the state of that item in the array to "complete" if the user clicks on "complete".
Here is my action:
export function completeTodo(id) {
return {
type: "COMPLETE_TASK",
completed: true,
id
}
}
Here is my reducer:
case "COMPLETE_TASK": {
return {...state,
todos: [{
completed: action.completed
}]
}
}
The problem I'm having is the new state does no longer have the text associated of that todo item on the selected item and the ID is no longer there. Is this because I am overwriting the state and ignoring the previous properties? My object item onload looks like this:
Objecttodos: Array[1]
0: Object
completed: false
id: 0
text: "Initial todo"
__proto__: Object
length: 1
__proto__: Array[0]
__proto__: Object
As you can see, all I want to do is set the completed value to true.
You need to transform your todos array to have the appropriate item updated. Array.map is the simplest way to do this:
case "COMPLETE_TASK":
return {
...state,
todos: state.todos.map(todo => todo.id === action.id ?
// transform the one with a matching id
{ ...todo, completed: action.completed } :
// otherwise return original todo
todo
)
};
There are libraries to help you with this kind of deep state update. You can find a list of such libraries here: https://github.com/markerikson/redux-ecosystem-links/blob/master/immutable-data.md#immutable-update-utilities
Personally, I use ImmutableJS (https://facebook.github.io/immutable-js/) which solves the issue with its updateIn and setIn methods (which are more efficient than normal objects and arrays for large objects with lots of keys and for arrays, but slower for small ones).
New state does no longer have the text associated of that todo item on
the selected item and the ID is no longer there, Is this because I am
overwriting the state and ignoring the previous properties?
Yes, because during each update you are assigning a new array with only one key completed, and that array doesn't contain any previous values. So after update array will have no previous data. That's why text and id's are not there after update.
Solutions:
1- Use array.map to find the correct element then update the value, Like this:
case "COMPLETE_TASK":
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id ? { ...todo, completed: action.completed } : todo
)
};
2- Use array.findIndex to find the index of that particular object then update that, Like this:
case "COMPLETE_TASK":
let index = state.todos.findIndex(todo => todo.id === action.id);
let todos = [...state.todos];
todos[index] = {...todos[index], completed: action.completed};
return {...state, todos}
Check this snippet you will get a better idea about the mistake you are doing:
let state = {
a: 1,
arr: [
{text:1, id:1, completed: true},
{text:2, id:2, completed: false}
]
}
console.log('old values', JSON.stringify(state));
// updating the values
let newState = {
...state,
arr: [{completed: true}]
}
console.log('new state = ', newState);
One of the seminal design principles in React is "Don't mutate state." If you want to change data in an array, you want to create a new array with the changed value(s).
For example, I have an array of results in state. Initially I'm just setting values to 0 for each index in my constructor.
this.state = {
index:0,
question: this.props.test[0].questions[0],
results:[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0]],
complete: false
};
Later on, I want to update a value in the array. But I'm not changing it in the state object. With ES6, we can use the spread operator. The array slice method returns a new array, it will not change the existing array.
updateArray = (list, index,score) => {
// updates the results array without mutating it
return [
...list.slice(0, index),
list[index][1] = score,
...list.slice(index + 1)
];
};
When I want to update an item in the array, I call updateArray and set the state in one go:
this.setState({
index:newIndex,
question:this.props.test[0].questions[newIndex],
results:this.updateArray(this.state.results, index, score)
});