Alter Object Array in React with useState-hook without deep copying - javascript

What is the best and clean way to alter Object Arrays?
I have a code that look´s like this.
const [get_post, set_post] = useState([
{name: "First"},
{name: "Second"},
{name: "Third"},
])
I would like to add and edit keys, on a certain index. So I do it like this:
<button onClick={()=>
set_post([...get_post, get_post[0].content = "This is index zero" ])
}> Manipulate! </button>
My result is this:
[
{"name": "First", "content": "This is index zero"},
{"name": "Second"},
{"name": "Third"},
"This is index zero" <-- This line is the problem
]
I have googled this a lot and this seems to be a common subject, however.
This post describe the same problem and solution with a keyed object, which doesn't help me.
React Hooks useState() with Object
This post support 3rd party libs and/or deep copying, which I suspect isn't the "right" way of doing it either.
Whats the best way to update an object in an array in ReactJS?
This thread also support a lot of deep copys and maps, which I suppose I don't need (It's an array, I'm should be able to adress my object by index)?
How do I update states `onChange` in an array of object in React Hooks
Another deep copy solution
https://codereview.stackexchange.com/questions/249405/react-hooks-update-array-of-object
The list goes on...
Basically I want the result I got without the extra line,
and if even possible:
Without deep copying the state to inject back in.
Without 3rd party libraries.
Without using a keyed object.
Without running a map/filter loop inside set_post.
Edit: The reason why map should be unnecessary in setPost.
In my particular scenario the Module that renders the getPost already is a map-loop. Trying to avoid nested loops.
(My logic simplified)
const [get_post, set_post] = useState([
{name: "First"},
{name: "Second"},
{name: "Third"},
])
//Render module
//Fixed numbers of controllers for each Object in Array.
get_post.map((post, index)=> {
<>
<button onClick={()=>
set_post([...get_post, get_post[index].content = "Content 1" ])}
}>
Controller 1
</button>
<button onClick={()=>
set_post([...get_post, get_post[index].content = "Content 2" ])}
}>
Controller 2
</button>
<button onClick={()=>
set_post([...get_post, get_post[index].content = "Content 3" ])}
}>
Controller 3
</button>
//...
</>
})

If you just want to alter the first property, extract it from the array first.
You can use the functional updates method to access the current state value and return a new one with the changes you want.
set_post(([ first, ...others ]) => [{
...first,
content: "This is index zero"
}, ...others])
To alter any particular index, you can map the current array to a new one, creating a new object for the target index when you reach it
let x = the_target_index
set_post(posts => posts.map((post, i) => i === x ? {
...post,
content: `This is index ${x}`
} : post))
A slightly different version of this that matches what you seem to want to do in your answer would be
set_post(posts => {
posts[x] = { ...posts[x], content: `This is index ${x}` }
// or even something like
// posts[x].content = `This is index ${x}`
return [...posts] // clone the array
})

I have found a solution that works! I haven't seen this posted elsewhere so it might be interesting to look into.
To change or add a key/value to an object in an array by index just do this:
<button onClick={() => {
set_post( [...get_post , get_post[index].content = "my content" ] )
set_post( [...get_post ] ) //<-- this line removes the last input
}
}>
As Phil wrote, the earlier code is interpreted as:
const val = "This is index zero";
get_post[0].content = val;
get_post.push(val);
This seems to remove the latest get_post.push(val)
According to the React Docs, states can be batched and bufferd to for preformance
https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly
When React batches set_post it should behave like this.
If the one-line command
set_post( [...get_post , get_post[index].content = "my content" ] )
gives
const val = "This is index zero";
get_post[0].content = val;
get_post.push(val);
The double inject would trigger the buffer and reinject the state at the end insted of array.push(). Something like this.
var buffer = get_post
buffer[0].content = val
//Other updated to get_post...
get_post = buffer //(not push)
There for, this should be a perfectly good solutions.

Related

How can I map through several arrays of objects using JSX in React?

I have some json data:
{
"shops" :
[
{
"type" : "Blacksmith/Armoury",
"items_light_armour" :
[
"Light Armour",
{
"id": 1,
"item" : "Padded Armour",
"low_price": 15,
"med_price": 20,
"high_price": 30,
"available": true,
"limited": false,
"weight": 8,
"blurb": "AC11. Made of quilted layers of cloth and batting."
},
//...
//three other objects, as array elements, with a similar set of key/value pairs
The first element (inside the items_light_armour array) is just a string to denote what category each data set is.
What I want to do, is display every single object (apart from its ID and blurb) in a table I have. The table generates new rows for every item mapped through.
The problem I have is that it's not mapping out the data as intended.
Here's a picture of what the table looks like at when it first loads:
So the data does map to the table, but only the first item in each array of objects. Im honestly unsure of how to get all of the data for each category.
Specifically, each category should list all of its items, then a new category would open up, list all of its entries like the first and so on until the end of the array is reached, instead of one from each as displayed above.
Here's how I get my data from the JSON file (above my return block in the component):
//get the json data
const jsonData = require("../Reference Files/Json Files/shops.json");
Then, I convert it to a const
const objInnerValues = Object.values(jsonData.shops[0])
Then, in the return block of my component, here's how I 'fill' the table rows.
EDIT: This is the latest code, following advice from #John:
//...
{
<>
objInnerValues.map((things, outerIndex) => (
{outerIndex === 0 ? console.log("outerIndex WAS 0"):
<TableRow key= {outerIndex} onClick={() => BarkItemBlurb(outerIndex)}>
{Object.values(things).map((eachThing, innerIndex) => (
{innerIndex == 0 ? console.log("innerIndex or outerIndex was 0"):
<>
<SuperTD key={innerIndex}>{eachThing.item}</SuperTD>
<SuperTD key={innerIndex+1}>{eachThing.weight}lbs</SuperTD>
<SuperTD key={innerIndex+2}>{eachThing.low_price}gp</SuperTD>
<SuperTD key={innerIndex+3}>{eachThing.med_price}gp</SuperTD>
<SuperTD key={innerIndex+4}>{eachThing.high_price}gp</SuperTD>
</>
})
</TableRow>
})
</>
}
//...
I'm using styled-components, hence the weird html tag names.
Any advice would be greatly appreciated.
This is probably want you want. When you get thing, you map that and you don't need to index it because eachThing is the actual object.
{
objInnerValues.map((things, i) => (
{things.map((eachThing) => (
<TableRow key= {i} onClick={() => BarkItemBlurb(i)}>
<SuperTD>{eachThing.item}</SuperTD>
<SuperTD>{eachThing.weight}lbs</SuperTD>
<SuperTD>{eachThing.low_price}gp</SuperTD>
<SuperTD>{eachThing.med_price}gp</SuperTD>
<SuperTD>{eachThing.high_price}gp</SuperTD>
</TableRow>
)}
))}
Given I haven't ran this due to lack of working code; I may have the wrong number of brackets/closing brackets and may have it in the wrong place but overall it should guide you to a solution.
Thanks to #John for the assist.
I overcomplicated my design. My JSON file contained arrays and objects, making it awkward to navigate.
After restructuring it to contain only arrays, it was a lot easier.
Also, as for the actual problem, I was shown that I needed to add another map function inside my original one to make it spit the data out the right way.
//...
Object.values(thing).map((innerThing, innerIndex) => (
<>
{console.log("\nouter/inner: "+outerIndex +", " +innerIndex)}
{console.log("\nData: \n\t"+innerThing[2] +"\n\t" +innerThing[8] + "\n\t" +innerThing[3]+"\n\t" + innerThing[4]+"\n\t"+innerThing[5])}
<TableRow key = {thing[0].toString()}>
<SuperTD>{innerThing[2]}</SuperTD>
<SuperTD>{innerThing[8]}lbs</SuperTD>
<SuperTD>{innerThing[3]}gp</SuperTD>
<SuperTD>{innerThing[4]}gp</SuperTD>
<SuperTD>{innerThing[5]}gp</SuperTD>
</TableRow>
</>
//...
Now, with the mapping sorted, this is the result:

React - How to map from two SQL tables simultaneously?

I am pulling from a SQL table device, and displaying its content to a table by mapping from device. I am trying to add a column that pulls information from another SQL table, group, but I haven't figured out how to adjust the mapping in order to pull from both device and group. I am sure the issue is caused since group isn't declared in this scope but I cannot solve how it should be declared in this portion of the script.
Both tables shared a common column, group_id, and I have added useSelector for both:
const device = useSelector((state) => state.device);
const group = useSelector((state) => state.group);
<Table
tableHeaderColor="warning"
tableHead={['Device Name', 'Location', 'Group', 'Release Version']}
tableData={device.deviceData.map((device) => {
return [
device['device_name'],
device['location_name'],
group['group_name'],
device['release'],
];
})}
/>
An alternative fix I have tried is finding the group_name since both tables device and group share the group_id column, but it causes a group.find is not a function error. I am unsure if my syntax is incorrect, as I'm working from this site as a resource.
tableData={device.deviceData.map((device) => {
return [
device['device_name'],
device['location_name'],
group.find(group => group.group_id === device.group_id).group_name,
device['release'],
];
})}
Many thanks for any advice
UPDATE:
Thank you for the answers and comments so far. Here is some additional information:
The reducer does contain the initial state empty array for group (groupdata)
const initialState = {
groupData: [],
result: '',
};
Here are the screenshots of the SQL tables device and group. They do not have the same number of entries, as group lists the groups that a number of devices can be assigned to. Hence there are many more entries under device than group.
device SQL table
group SQL table
If you using group.find you need to make sure the reducer contains the initial state empty array for group
Doing this in a map in the actual JSX might get a bit confusing since you're pulling from two different datasources. Also, assuming that the array in
device.deviceData and the group array share the same number of entries and correlate to each other, searching in group on each loop through device.deviceData seems like an unnecessary performance hit. My preference might be to create my source data in a plain for loop outside of the return JSX, and then just plug it in directly:
let tableData = []
for (let i = 0; i < device.deviceData.length; i += 1) {
const currentDevice = device.deviceData[i];
const currentGroup = group[i];
const entry = [
currentDevice['device_name'],
currentDevice['location_name'],
currentGroup['group_name'],
currentDevice['release'],
];
tableData.push(entry);
}
Then I would simply pass tableData to the <Table /> tableData prop.
I think group is an object and has a structure similar to device.deviceData. By that assumption, group.groupData should be the array which will be useful to us.
What we can do is build a Map of group_id -> group_name which we use later for getting relevant device group_name in Table component.
Here is a code snippet which should give you an idea (this is a JS based answer. I think your use-case can make use of joins at sql level for less work here) :-
const device = {
deviceData: [{
group_id: 1
},
{
group_id: 2
}
]
}
const group = {
groupData: [{
group_id: 1,
group_name: 'Group-1'
},
{
group_id: 2,
group_name: 'Group-2'
}
]
}
const buildMap = () => {
const gMap = {};
for (const {
group_id,
group_name
} of group.groupData) {
gMap[group_id] = group_name;
}
return gMap;
}
const groupMap = buildMap();
device.deviceData.forEach(d => console.log(groupMap[d.group_id]));

react native how to merge two arrays with a function

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']

ReactJS Array.push function not working in setState

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.

Does updating 1 element in an array trigger a render in React for other elements in array?

I am using React with Redux for my current project.
I have a state object like so:
state: {
reducerOne: {
keyOne: valueOne,
keyTwo: valueTwo
}
reducerTwo: {
keyThree: valueThree
keyFour: valueFour
}
}
Suppose valueFour is an array of objects. I use the valueFour array to map a bunch of React elements like so in the render method:
this.props.valueFour.map(()=> <div/>)
(Obviously the above is simplified but all that I am trying to indicate is that I am rendering a bunch of elements)
Now, I would like to update only 1 of the above elements. In my action/reducer code I return the entire valueFour array with the 1 element updated in a function like so:
action code
export function updateThingInArray(elementId){
return (dispatch, getState) => {
const myArray = getState().reducerTwo.keyFour
for (let i=0; i < myArray.length; i++) {
if (myArray[i].id === elementId){
const result= [
...myArray.slice(0,i),
Object.assign({},
myArray[i],
{field:newValue}),
...myArray.slice(i+1)
]
dispatch(update(result))
}
}
}
}
reducer code:
export default handleActions({
[UPDATE]: (state,{payload}) => Object.assign({},state, {keyFour: payload})
},{
keyThree:{},
keyFour:{}
})
(My reducer code uses the library redux-actions)
I hope all this makes sense. If it doesn't please ask and I will clarify.
But my question is, since keyFour has the entireArray technically updated, does that mean ALL my elements will update even though only 1 has new data? If so, is there a more efficient way of implementing what I am currently doing?
The render method will be called again, which means it will map through that array again. You can look at the elements tab in chrome and you can see the html that flashes, it is the part that changed. If the component has the same data and the same key, the actual html won't be changed, but the render method will be called in the children.
Edit: didn't realize you were updating a nested key, React only checks shallow equality: https://facebook.github.io/react/docs/shallow-compare.html

Categories