My React App displays a grid of names for selection. When I select a name it reads a database and shows rows of data applicable to that name. With each row being an object, which is stored as a single array element.
I have two arrays to contain the objects, one is the 'original data' and the other is the 'modified data'. Then I compare these to see if there has been a change to one of the rows before updating the database.
The arrays are defined so:
constructor(props) {
super(props);
this.state = {
...
detail_data: [],
old_detail_data: []
}
}
When I select a row a call is made to _handleRowClick():
_handleRowClick(i) {
if (i >= 0) {
this.getRecord(i, (err, res) => {
if (!err) {
let detail = res.body;
this.setState({
name_detail: Object.assign({}, detail), // Stores details about the name
selectedIndex: i
})
// Fetch the Career Directions
this.fetchSingleCareerDirections(this.state.detail.animal_code); // Gets the rows
}
})
}
}
fetchSingleCareerDirections(animal_code) {
request.get(`/lookup/career_directions/${animal_code}`, (err, res) => {
let data = [].concat(res.body); // row data
this.setState({
detail_data: [...data], // array 1
old_detail_data: [...data], // array 2
});
});
}
At this point all is well and my data is as expected in detail_data and old_detail_data. So I modify one piece of data in one row, in this case clicking a checkbox (for valid career), but any change to row data has the same effect:
<td>
<input type="checkbox"
checked={item.valid_career == 'Y' ? true : false}
style={{ width: 30 }}
name={"valid_career|" + i}
onChange={(e) => { this._setTableDetail("valid_career", e.target.checked == true ? 'Y' : 'N', i) }}
/>
</td>
Which calls the update routine _setTableDetail() to store a 'Y' or 'N' into the detail_data array:
_setTableDetail(name, value, index) {
let _detail_data = Object.assign([], this.state.detail_data);
_detail_data[index][name] = value;
this.setState({ detail_data: _detail_data });
}
This updates this.state.detail_data as expected. But if I look at this.state.old_detail_data the exact change has also been made to that array. Likewise, as a test, if I modify old_detail_data that updates detail_data.
This MUST be happening because the two arrays both reference the same memory space. But I cannot see how that is happening. My setState() routine, as seen above, does this:
this.setState({
detail_data: [...data],
old_detail_data: [...data],
});
Which, to my understanding, uses the spread operator to create a new array in each instance. So how are these two arrays both referencing the same memory space? Is it something to do with me doing the cloning inside the setState() call maybe?
Thanks very much #Anthony for your comment. You were quite right in where I was going wrong. So although my arrays were unique their contained objects were referencing the same objects in memory.
I modified the code of the fetchSingleCareerDirections() function to resolve the issue:
fetchSingleCareerDirections(animal_code) {
request.get(`/lookup/career_directions/${animal_code}`, (err, res) => {
let data = [].concat(res.body);
this.setState({
detail_data: data.map(row => Object.assign({}, row)), // array 1
old_detail_data: data.map(row => Object.assign({}, row)), // array 2
});
});
}
The program now works perfectly.
Just one point to note. I am using a slightly older version of JavaScript for which the spread operator didn't work (sadly). But testing this elsewhere beforehand I was able to use Anthony's code, as suggested, which is a better more modern approach:
fetchSingleCareerDirections(animal_code) {
request.get(`/lookup/career_directions/${animal_code}`, (err, res) => {
let data = [].concat(res.body);
this.setState({
detail_data: data.map(row => ({ ...row })), // spread operator
old_detail_data: data.map(row => ({ ...row })), // spread operator
});
});
}
Related
I am fetching my data from external API as usual and this is the typical way I do it:
Fetch API:
const [tshirts, setTshirts] = useState([]);
const fetchData = () => {
fetch('apiEndpoint')
.then((response) => response.json())
.then((data) => {
setTshirts(data[0].clothes.regular.top); // path to my array
})
.catch((error) => {
console.log(error);
});
};
React.useEffect(() => {
fetchData();
}, []);
Map through an array:
const tshirtArray = tshirts.tShirt; // specifying the path
const listItems = tshirtArray.map((item) => <li>{item}</li>);
<ul>{listItems}</ul>
Example of data structure:
[
{
id: 1,
clothes: {
regular: {
top: {
sleeveless: [],
tShirt: [
"image-path-here"
],
.....
.....
.....
When I first time execute the code it works, but after some time or after refreshing the page I get an error of TypeError: Cannot read properties of undefined (reading 'map')
Why is that undefined? The path is correct and fetching the array should be as well. Can not find the reason of it not working.
I don't have reputation to comment, so let me try to clarify it for you through an answer. As #sojin mentioned, you cannot use tshirts.Tshirt since your state is of array type and arrays can't be used like objects, meaning that if there was an object of lets say exampleObject = { type: "shirt", color: "white } you could call it with exampleObject.type. Since you have an array of objects in your state (top that you are saving to state is still object which contains tShirt array), you first have to use index (to tell which object you want to use from the state array) and then you can use it like you wanted. For example, in your example there are 1 objects in state array. Array indexes start at 0. So you could do tshirts[0].tShirt to get the tShirt array from that object.
However, I would edit your code a bit. Instead of using tshirtArray constant, just do listItems from your state:
const listItems = tshirts.map((item) => {item.tShirt[0]});
Note: I've just used index 0 here to demonstrate the finding of the first item in tShirt array. If you want to see all tShirt image paths, then you may need to do nested mapping or other similar solutions.
I have the following state in my app:
const [education, setEducation] = useState([
{
institution: "University",
area: "President",
studyType: "Bachelor",
startDate: "2013-01-01",
endDate: "2014-01-01",
gpa: "4.0",
courses: ["DB1101 - Basic SQL"],
},
]);
And the following method to update the state:
const onCoursesChange = (newValue, index) => {
const newValuesArr = newValue ? newValue.map(item => item.value) : [];
setEducation((prev)=>(
prev[index].courses=newValuesArr
));
//setEducation(education[index].courses = newValuesArr);
console.log(education[index].courses)
};
With the above code I get the error Cannot create property 'courses' on string 'asdfa' when the array newValuesArr has more than one element.
How do I update the state ??
The problem is here:
setEducation((prev)=>(
prev[index].courses=newValuesArr
));
prev[index].courses=newValuesArr is an assignment that simply returns the value being assigned, here newValuesArr. This is an array of strings, so its elements do not have the properties you expect.
Your state-updating function (the one passed to setEducation) instead needs a return the whole of prev, with the update you desire. And this should be done immutably - that is, without updating prev. Luckily, ES6 offers nice tools for doing this. Here is how you can do it in this case:
setEducation((prev)=>(
prev.map((item, oldIndex) => oldIndex === index ? { ...item, courses: newValuesarr } : item )
));
That is, it maps through the array prev, and leaves all unchanged apart from the one with the matching index, and in that one updates the courses property to the desired array.
How to add value to a specific object to the array by the index?
I wrote this, but of course, it creates a new object in the array, but I want to insert "errors" to an existing object with index (on screen it 0 index)
ipcRenderer.on('fileData', (event, data) => {
this.setState({jobs: [...this.state.jobs, {errors: data}]})
});
Then i wrote this:
ipcRenderer.on('fileData', (event, data) => {
this.state.jobs.forEach((item, index) => {
this.setState({jobs: [...this.state.jobs, {errors: item[index] = data}]
})
console.log(this.state)
})
});
It inserts a value into the object, but without a name and it still creates a new element in the array
I want the result to be like this:
jobs: [
0: {errors: 10, fileName:...}
]
If you know the index, you can just do
const jobs = this.state.jobs.slice(0);
jobs[index].errors = data;
this.setState({jobs});
Might have to do more than slice the array, might have to make a deep copy, but yeah, that should work.
Firstly you can make a copy of your array like
let jobsCopy = this.state.jobs
Then if you know the index you could just do like
jobsCopy[index].errors = 10
this.setState({
jobs: jobsCopy
})
You would need to know the index of the object you want to change. For example if you know it is the first item in the array you can do this:
const indexToChange = 0
this.setState(prevState => prevState.map((obj, i) => {
if(i === indexToChange) {
return {
...obj,
errors: data
}
} else {
return obj
}
}))
I cannot figure out what I missed on line row.sections[SECTION_ID. It always show me a typo error ','...
FAQ: sections - is an array with objects inside. In this case I'm
trying modify the specific object of the sections founded by custom
flag SECTION_ID.
P.S.
I also tried to put row.sections[SECTION_ID] inside an extra brackets [], but unfortunately it does not help... Any solutions?
rows: state.rows.map(
row =>
row.ID === action.rowID
? {
...row,
sections: [
...row.sections,
row.sections[SECTION_ID]: { // error is here
...row.sections[SECTION_ID],
data: {
...// some data
}
}
]
}
: row
)
You cannot mutate some element inside array by spread operation in such way. Using this approach you'll jus add a new, mutated element to the same array each time. So, it you want to make it right, you need to use the map iterator instead:
rows: state.mymaps.rows.map(
row =>
row.ID === action.rowID
? {
...row,
sections: row.sections.map(
(section, index) =>
index === JOIN_SECTION_ID
? {
...section,
data: {
...section.data
}
} : section
)
} : row
)
If you're trying to replace an element at a certain index of the array without mutating the array, you can make a shallow copy of the array, and then set the value at that index. For example:
state.rows.map((row) => {
if (rowID !== action.rowID) {
return row;
}
const sectionCopy = [...row.sections];
sectionCopy[SECTION_ID] = {
...row.sections[SECTION_ID],
data: {
// some data
},
};
return {
...row,
sections: sectionCopy,
};
});
I have a todo list that holds a delete button in a grandchild, that when clicked fires an event in the parent - I am wanting this event to delete the array entry corresponding to the grandchild clicked.
Parent (contains the array and my attempt at the function)
const tasks = [
{ name: 'task1', isComplete: false },
{ name: 'task2', isComplete: true },
{ name: 'task3', isComplete: false },
]
// taskToDelete is the name of the task - doesn't contain an object
deleteTask(taskToDelete) {
this.state.tasks.remove(task => task.name === taskToDelete);
this.setState({ tasks: this.state.tasks });
}
Any help would be appreciated
Two issues there:
You're seeming to try to direct modify this.state.tasks. It's important not to do that, never directly modify this.state or any object on it. See "Do Not Modify State Directly" in the React documentation for state.
You're passing an object to setState that is derived from the current state. It's important never to do that, too. :-) Instead, pass setState a function and use the state object it passes you when calling that function. From "State Updates May Be Asynchronous" in the documentation:
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state... [Instead]...use a second form of setState() that accepts a function rather than an object.
(my emphasis)
I figure your remove on an array was intended to be hypothetical, but for the avoidance of doubt, arrays don't have a remove method. In this case, the best thing to do, since we need a new array, is to use filter to remove all entries that shouldn't still be there.
So:
deleteTask(taskToDelete) {
this.setState(prevState => {
const tasks = prevState.tasks.filter(task => task.name !== taskToDelete);
return { tasks };
});
}
You could simply filter the array :
this.setState(prevState => ({
tasks: prevState.tasks.filter(task => task.name !== 'taskToDelete')
}));
Also when updating based on this.state, its better to use the function form because setState is async.
You can use filter to remove one object from an array following the immutable pattern (filter will create a new array) :
deleteTask(taskToDelete) {
const newTaskArray = this.state.tasks.filter(task => task.name !== taskToDelete);
this.setState({ tasks: newTaskArray });
}
Edit : codepend of the solution : https://codepen.io/Dyo/pen/ZvPoYP
You can implement deleteTask method as below:
deleteTask(taskToDelete) {
this.setState((prevState, props) => {
const tasks = [...prevState.tasks];
const indexOfTaskToDelete = tasks.findIndex(
task => task.name === taskToDelete
);
tasks.splice(indexOfTaskToDelete, 1);
return { tasks };
});
}
A. Find the index of taskToDelete.
B. Then use splice method to delete the item from the collection
C. Then call setState to update the state with tasks.
You can use higher order function Array#filter to delete the task.
let updatedTasks = this.state.tasks.filter(task => task.name !== taskToDelete);
this.setState({ tasks: updatedTasks });
I have followed below steps to delete a particular selected Object from the state array:
Here I am using a list of checkBoxes, when I am selecting a checkBox it will add it in the state array and when it gets de-selected then it will get deleted from the array.
if (checked) {
var tempObject = { checkboxValue: data, label: title }
this.state.checkBoxState.push(resTemp);
} else {
var element = data; //data is coming from different method.
for (let index = 0; index < this.state.checkBoxState.length; index++) {
if (element === this.state.checkBoxState[index].checkboxValue) {
this.state.checkBoxState.splice(index, 1);
}
}
}
I got stuck for this question and I am sharing my solution. Hope it will help you.