I have a case that I need to update a value in hierarchy state variable (which consists of 3 tree levels in my case, and I need to update a value in third level for example).
I copied that state variable to local variable and did the update on it, but I noticed that state variable was changed as well!
Searched for it, and saw suggestions for using Object.assign or Spread operator, I tried using the spread but it didn't help with creating a copy of the state to prevent both objects referring to the same reference but it didn't work!
Object.assign appraoch didn't work neither!
Ho can I accomplish that?
const assign = require('object-assign');
// let properties = this.state.configProperties
// let properties = assign({}, this.state.configProperties)
let properties = { ...this.state.configProperties };
properties.Children.forEach((lvl1) => {
lvl1.Properties.forEach((property) => {
if (property.id === id) {
property.value = val;
return;
}
});
lvl1.Children.forEach((lvl2) => {
lvl2.Properties.forEach((property) => {
if (property.id === id) {
property.value = val;
return;
}
});
lvl2.Children.forEach((lvl3) => {
lvl3.Properties.forEach((property) => {
if (property.id === id) {
property.value = val;
return;
}
});
});
});
});
You have to clone the nested level objects/arrays as well, in order to avoid referencing original values.
Here is an example on how to achieve deeply nested objects copying,
import React from "react";
import "./styles.css";
export default function App() {
const state = {
dataOne: {
subDataOne: {
text: "Test",
value: "Test One"
},
subDataTwo: {
text: "Test",
value: "Test One"
}
},
dataTwo: {
subDataOne: {
text: "Test",
value: "Test One"
},
subDataTwo: {
text: "Test",
value: "Test Two"
}
}
};
const clonedState = { ...state };
const clonedData = { ...clonedState.dataOne };
const clonedSubDataOne = { ...clonedData.subDataOne };
clonedSubDataOne.text = "New Test Value";
clonedData.subDataOne = clonedSubDataOne
clonedState.dataOne = clonedData
console.log("Original =>",state)
console.log("Cloned =>",clonedState)
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
You can also use Lodash Library for cloning and many more.
import * as _ from 'lodash';
const copy = _.cloneDeep(object)
Related
const [Questions, setQuestions] = React.useState(props.QuestionsData);
const handleClick = (id, isCorrect) => {
if (isCorrect) {
setQuestions((prev) => {
prev.map((item, index) => {
if (index === id) {
return {
...item,
[item.Answers.correct.selected]: !item.Answers.correct.selected,
};
} else {
return item;
}
});
});
} else {
return;
}
};
This is the code im doing and what I want to do it loop over the Questions (which has a list of objects that look like this.
{
"category": "Entertainment: Video Games",
"type": "boolean",
"difficulty": "hard",
"question": "The first "Metal Gear" game was released for the PlayStation 1.",
"correct_answer": "False",
"incorrect_answers": [
"True"
],
"Answers": {
"correct": {
"MasterId": "MXTOfKnKw7dU7QP0UP0td",
"id": 0,
"answer": "False",
"selected": false,
"correct": true,
"userChosen": true
},
"wrong": {
"id": 1,
"answer": "True",
"selected": false
}
}
}
I tried to do everything but it returns undefined. I even tried to do the map alone with the value and logged out the needed value and it worked.
If someone can tell me a good way to loop over usestate array of objects and only edit the objects that satisfy the condition that would be great.
Remove the curly braces so that the value is actually returned.
setQuestions((prev) =>
prev.map((item, index) => {
if (index === id) {
return {
...item,
[item.Answers.correct.selected]: !item.Answers.correct.selected,
};
} else {
return item;
}
});
);
I don't recommend using the capitalization convention for variable names. Use camelCase and save those for Components, Classes, and Types.
For the sake of readability and clean code, I recommend you write data processing code outside of setSomeState()
Here's an example.
import { useState } from "react";
...
const [questionList, setQuestionList] = useState(props.questionsData);
const handleClick = (id, isCorrect) => {
if (isCorrect) {
const updatedList = questionList.map((item, index) => {
if (index === id) {
return {
...item,
[item.answers.correct.selected]: !item.answers.correct.selected,
};
} else {
return item;
}
});
// no messy prev state stuff, it's now clean and easy to maintain
setQuestionList(updatedList);
} else {
return;
}
};
Further, I see that you use props.questionData as a default value of the setState. It's not a good approach since one component's state's default value depends on the other component's value and can potentially cause errors.
I personally recommend you to keep the questions default state to an empty array, and use useEffect and do setQuestions inside the useEffect with props.questionData.
Can you imagine writing this handler every time you have array-based stated? Also map doesn't make sense because we only want to update one item, however map iterates over the entire array.
Write a generic function to update an arr at a specific index using a user-supplied func -
function update(arr, index, func) {
return [
...arr.slice(0, index),
func(arr[index]),
...arr.slice(index + 1)
]
}
Now in your component -
function MyComponent({ questions = [] }) {
const [questions, setQuestions] = useState(questions)
const updateQuestion = index => event =>
setQuestions(arr => update(arr, index, q => ({
/* update q here */
})))
return questions.map((q, index) =>
<Question key={index} question={q} onClick={updateQuestion(index)} />
)
}
Developing your own utility functions to operate on arrays and objects can be a fun exercise, but maybe challenging to get right. Look to popular libraries like Immutable.js and Immer for inspiration.
See these Q&A for live examples you can run in your browser -
Add Numbers in ReactJS without button
How to change Active when it is clicked
How to remove an element from an array passing the key using hooks?
how to add class and remove class from buttons in react?
You forgot to return the result of the map. If a function doesn't return anything in JS, undefined will be returned. In addition, the computed property isn't used correctly here:
{
...item,
[item.Answers.correct.selected]: !item.Answers.correct.selected,
}
Try this:
const handleClick = (id, isCorrect) => {
if (isCorrect) {
setQuestions((prev) =>
prev.map((item, index) => {
if (index === id) {
item.Answers.correct.selected = !item.Answers.correct.selected
}
return item
}));
}
};
I try to check if an ID from an Array of Objects is currently in my cart Array which contains Objects too. Is there any way to make it work with good performance?
React also shows an Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
My Code:
import { useState } from "react";
const UpsellItems = [
{
searchId: "Cart Id 1",
suggestionId: "Id 1",
message: "1",
},
{
searchId: "Cart Id 2",
suggestionId: "Id 2",
message: "2",
},
];
const CartUpsell = ({ cart }) => {
const [upsell, setUpsell] = useState("");
UpsellItems.map((UpsellItem) => {
for (var i = 0; i < cart.length; i++) {
if (cart[i]._id === UpsellItem.searchId) {
setUpsell(UpsellItem.message);
break;
}
}
});
return <div className="">{upsell}</div>;
};
export default CartUpsell;
Never call setState inside component. It need to call inside a function or hooks effect.
In this case, you should call in the useEffect:
useEffect(() => {
UpsellItems.map((UpsellItem) => {
for (var i = 0; i < cart.length; i++) {
if (cart[i]._id === UpsellItem.searchId) {
setUpsell(UpsellItem.message);
break;
}
}
});
}, [])
I'm still learning react. The data is retrieved from the redux action and store as props. My problem is that my variable is undefined after a filter function executed. What I am trying to do is using the data from redux action, and display those variable. The state of the component turn out to be undefined and nothing display on view. Does anyone know how to fix this?
https://i.stack.imgur.com/3xyJn.png
1) Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
2) Uncaught TypeError: Cannot destructure 'this.state.currentTour' as it is undefined.
[
{
"_id": "12345",
"name": "I am first tour",
"description": "Iasofisajdaiosjdioasdmoias",
"imageUrl": "https://something1.jpg",
"dates": [
"2021-06-19T09:00:00.000Z",
"2021-07-20T09:00:00.000Z",
],
},
{
"_id": "67890",
"name": "I am second tour",
"description": "cvxbcvbcxbcvbcxb",
"imageUrl": "https://something2.jpg",
"dates": [
"2023-01-12T09:00:00.000Z",
"2023-04-22T01:00:00.000Z",
],
},
//.....rest data
]
import React, { Component } from 'react';
import './Tour.css';
import { connect } from 'react-redux';
class Tour extends Component {
constructor(props) {
super(props)
this.state = {
currentTour: {},
}
this.findSingletour = this.findSingletour.bind(this);
}
componentDidUpdate() {
const tourId = this.props.match.params._id;
let FilteredTour = this.findSingletour(tourId);
// console.log(FilteredTour); ----> undefined
if (FilteredTour !== this.state.currentTour) {
this.setState({
currentTour: FilteredTour
});
}
}
findSingletour = (tourId) => {
const notYetFilterTours = this.props.tours.tourState.data;
let filtered = [];
if (notYetFilterTours) {
filtered = notYetFilterTours.find((tour) => {
if (tour.id === tourId) return true;
return filtered; // ---> actual object get back { id: '...' , name: '...', ..... }
});
}
};
render() {
const {
name,
description,
imageUrl,
dates,
} = this.state.currentTour || {}; // ---> undefined
return (
<div>
<span>{name}</span>
<span>{description}</span>
<span>{imageUrl}</span>
<span>{dates[0]}</span>
</div>
)
}
}
const mapStateToProps = (state) => ({
tours: state.tourContainer,
});
export default connect(
mapStateToProps,
)(Tour);
Try this, I don't know does it helpful or not, but it's work for me
For warning Can't perform a React state update...
=> to not see this warning add code below, and add if(!this.mounted) return; before where ever you use this.setState
private mounted = false as boolean;
componentWillUnmount() {
this.mounted = false
}
componentWillMount() {
this.mounted = true
}
I see your function findSingletour() should return default value for it.
Example:
findSingletour = (tourId) => {
const notYetFilterTours = this.props.tours.tourState.data;
let filtered = [];
if (notYetFilterTours) {
filtered = notYetFilterTours.find((tour) => {
if (tour.id === tourId) return true;
return filtered; // ---> actual object get back { id: '...' , name: '...', ..... }
});
}
return filtered; // or something else cause I saw you had return bool or filtered
// If you do not return here result maybe is undefined
};
I am having a onChange function i was trying to update the array optionUpdates which is inside of sentdata by index wise as i had passed the index to the onChange function.
Suppose i update any two values of the input field from option which is inside of postdata therefore the input name i.e. orderStatus with changed value and with order should be saved inside of optionUpdates
For example: Suppose i update the option 1 and option 3 of my postdata further inside of options of orderStatus values so my optionUpdates which is inside of sentdata should look like this
optionUpdates: [
{
Order: 1,
orderStatus: "NEW1"
},
{
Order: 3,
orderStatus: "New2"
}
]
here is what i tried
setSentData(oldValue => {
const curoptions = oldValue.sentdata.optionUpdates[idx];
console.log(curoptions);
curoptions.event.target.name = event.target.value;
return {
...oldValue,
sentdata: {
...oldValue.sentdata.optionUpdates,
curoptions
}
};
});
};
Demo
complete code:
import React from "react";
import "./styles.css";
export default function App() {
const x = {
LEVEL: {
Type: "LINEN",
options: [
{
Order: 1,
orderStatus: "INFO",
orderValue: "5"
},
{
Order: 2,
orderStatus: "INPROGRESS",
orderValue: "5"
},
{
Order: 3,
orderStatus: "ACTIVE",
orderValue: "9"
}
],
details: "2020 N/w UA",
OrderType: "Axes"
},
State: "Inprogress"
};
const [postdata, setPostData] = React.useState(x);
const posting = {
optionUpdates: []
};
const [sentdata, setSentData] = React.useState(posting);
const handleOptionInputChange = (event, idx) => {
const target = event.target;
setPostData(prev => ({
...prev,
LEVEL: {
...prev.LEVEL,
options: prev.LEVEL.options.map((item, id) => {
if (id === idx) {
return { ...item, [target.name]: target.value };
}
return item;
})
}
}));
setSentData(oldValue => {
const curoptions = oldValue.sentdata.optionUpdates[idx];
console.log(curoptions);
curoptions.event.target.name = event.target.value;
return {
...oldValue,
sentdata: {
...oldValue.sentdata.optionUpdates,
curoptions
}
};
});
};
return (
<div className="App">
{postdata.LEVEL.options.map((item, idx) => {
return (
<input
key={idx}
type="text"
name="orderStatus"
value={postdata.LEVEL.options[idx].orderStatus}
onChange={e => handleOptionInputChange(e, idx)}
/>
);
})}
</div>
);
}
If I've understood correctly then what you're looking to do is save a copy of the relevant options object in sentdata every time one changes. I think the best way to approach this is by doing all your state modification outside of setPostData, which then makes the results immediately available to both setPostData and setSentData. It will also make the setters easier to read, which is good because you have some quite deeply nested and complicated state here.
A few other things worth noting first:
Trying to use synchronous event results directly inside the asynchronous setter functions will throw warnings. If you do need to use them inside setters, then it is best to destructure them from the event object first. This implementation uses destructuring although it didn't end up being necessary in the end.
You seem to have got a bit muddled up with setSentData. The oldValue parameter returns the whole state, as prev in setPostData does. For oldValue.sentdata you just wanted oldValue. You also wanted curoptions[event.target.name], not curoptions.event.target.name.
So, on to your code. I would suggest that you change the way that your input is rendered so that you are using a stable value rather than just the index. This makes it possible to reference the object no matter which array it is in. I have rewritten it using the Order property - if this value is not stable then you should assign it one. Ideally you would use a long uuid.
{postdata.LEVEL.options.map(item => {
return (
<input
key={item.Order}
type="text"
name="orderStatus"
value={item.orderStatus}
onChange={e => handleOptionInputChange(e, item.Order)}
/>
);
})}
The handleOptionInputChange function will now use this Order property to find the correct objects in both postdata and sentdata and update them, or if it does not exist in sentdata then push it there. You would do this by cloning, modifying, and returning the relevant array each time, as I explained before. Here is the function again with comments:
const handleOptionInputChange = (event, orderNum) => {
const { name, value } = event.target;
/* Clone the options array and all objects
inside so we can mutate them without
modifying the state object */
const optionsClone = postdata.LEVEL.options
.slice()
.map(obj => Object.assign({}, obj));
/* Find index of the changed object */
const optionIdx = optionsClone.findIndex(obj => obj.Order === orderNum);
/* If the orderNum points to an existing object...*/
if (optionIdx >= 0) {
/* Change the value of object in clone */
optionsClone[optionIdx][name] = value;
/* Set postdata with the modified optionsClone */
setPostData(prev => ({
...prev,
LEVEL: {
...prev.LEVEL,
options: optionsClone
}
}));
/* Clone the optionUpates array and all
contained objects from sentdata */
const updatesClone = sentdata.optionUpdates
.slice()
.map(obj => Object.assign({}, obj));
/* Find the index of the changed object */
const updateIdx = updatesClone.findIndex(obj => obj.Order === orderNum);
/* If the changed object has already been
changed before, alter it again, otherwise push
a new object onto the stack*/
if (updateIdx >= 0) {
updatesClone[updateIdx][name] = value;
} else {
updatesClone.push({ Order: orderNum, [name]: value });
}
/* Set sentdata with modified updatesClone */
setSentData(prev => ({
...prev,
optionUpdates: updatesClone
}));
}
};
I have an array like below
[
1:false,
9:false,
15:false,
19:false,
20:true,
21:true
]
on click i have to change the value of specific index in an array.
To update value code is below.
OpenDropDown(num){
var tempToggle;
if ( this.state.isOpen[num] === false) {
tempToggle = true;
} else {
tempToggle = false;
}
const isOpenTemp = {...this.state.isOpen};
isOpenTemp[num] = tempToggle;
this.setState({isOpen:isOpenTemp}, function(){
console.log(this.state.isOpen);
});
}
but when i console an array it still shows old value, i have tried many cases but unable to debug.
This is working solution,
import React, { Component } from "react";
class Stack extends Component {
state = {
arr: [
{ id: "1", value: false },
{ id: "2", value: false },
{ id: "9", value: false },
{ id: "20", value: true },
{ id: "21", value: true }
]
};
OpenDropDown = event => {
let num = event.target.value;
const isOpenTemp = [...this.state.arr];
isOpenTemp.map(item => {
if (item.id === num) item.value = !item.value;
});
console.log(isOpenTemp);
this.setState({ arr: isOpenTemp });
};
render() {
let arr = this.state.arr;
return (
<React.Fragment>
<select onChange={this.OpenDropDown}>
{arr.map(item => (
<option value={item.id}>{item.id}</option>
))}
</select>
</React.Fragment>
);
}
}
export default Stack;
i hope it helps!
The problem is your array has several empty value. And functions like map, forEach will not loop through these items, then the index will not right.
You should format the isOpen before setState. Remove the empty value
const formattedIsOpen = this.state.isOpen.filter(e => e)
this.setState({isOpen: formattedIsOpen})
Or use Spread_syntax if you want to render all the empty item
[...this.state.isOpen].map(e => <div>{Your code here}</div>)