How to change update an object within a useState array - javascript

Suppose we have an array of objects in userInformation:
[
{
firstName:'Bob',
lastName:'Dude',
},
{
firstName:'John',
lastName:'Rad',
}
]
const [userInformation, setUserInformation] = useState([]);
userInformation.forEach((user, index) => {
if (snapshot.val().leadID === user.leadID) {
setUserInformation((userInformation) => ({
...userInformation,
[index]: snapshot.val(),
}));
}
});
I would like to update the second object.
My code doesn't seem to be working quite right. Any suggestions?

Yes few suggestions I have for you:)
First of all you have never assigned userinformation to your state.
So it should be some what like below
const [userInformation, setUserInformation] = useState(userinformation);
Or you must be getting userinformation from an API and using useEffect to initialize it.
Second thing is you must have an id as well as index key on each user something like this:
[
{
leadId:1,
firstName:'Bob',
lastName:'Dude',
index:1
},
{
leadId:2,
firstName:'John',
lastName:'Rad',
index:2
}
]
Now, Coming to what you are expecting you can use map() function in such as way that once the condition is met, you update that particular user, otherwise you should return back same users when condition is not met.
const updatedUsers = userInformation.map((user, index) => {
if (snapshot.val().leadID === user.leadID) {
return setUserInformation((userInformation) => ({
...userInformation,
[index]: snapshot.val(),
}));
}
return userInformation;
});

Here I think a simple find() would do the trick, rather than trying to loop the array.
const updateInfo = (id, obj /* snapshot.val() */) => {
const item = userInformation.find(({ leadID }) => leadID === id);
const updatedItem = {
...item,
...obj
};
setUserInformation((previousInfo) => {
...userInformation,
...updatedItem
})
}

Sorry for the lack of information provided in my question.
I was using firebase realtime with React, and not wrapping my logic in a useEffect was one problem.
I went with this solution:
setUserInformation(
userInformation.map((user) =>
user.leadID === snapshot.val().leadID
? {
...user,
...snapshot.val(),
}
: user
)
);
Thank you for all your answers!

Related

How to simplify updating state of Quiz component in React?

I have Quiz component with two types of questions (one correct answer and questions with free answer). I need to send answers to the backend in the following format:
[
{
questionId: 'test-id',
answers: ['answerId']// send answer id if this is question with one correct answer
},
{
questionId: 'test-id',
answers: ['answerId']// send answer id if this is question with one correct answer
},
{
questionId: 'test-id-2',
freeAnswer: 'some text' // send freeAnswer if it is open ended question
}
...
]
I create two handlers: one for text area and one for radiobuttons,
const handleOptionChange = ( question, answer) => {
onChangeQuestionAnswer(question, answer, 'oneCorrectAnswer')
}
const handleFreeAnswerChange = (value, question) => {
onChangeQuestionAnswer(question, value, 'freeAnswer')
}
And one general handler in parent component to process all answers:
const [chosenAnswers, setChosenAnswers] = useState([])
const handleChangeQuestionAnswer = (
questionId,
answerId,
type
) => {
const foundedId = chosenAnswers.find(item => item.id === questionId)
if (!foundedId) {
if (type === 'oneCorrectAnswer') {
setChosenAnswers([...chosenAnswers, { id: questionId, answers: [answerId] }])
} else {
setChosenAnswers([...chosenAnswers, { id: questionId, freeAnswer: answerId }])
}
} else {
const newResultArray = chosenAnswers.map(item => {
if (item.id !== questionId) {
return item
}
if (type === 'oneCorrectAnswer') {
return {
...item,
answers: [answerId]
}
} else {
return {
...item,
freeAnswer: answerId
}
}
})
setChosenAnswers(newResultArray)
}
}
Then I just sending chosenAnswers to API. This approach works, but it looks weird and overhead for me, can I somehow simplify this logic?
You can refactor your code by using ES6 feature to make it easy to read, and maybe split change answer handler of textarea and radiobutton is more better?
A function should only do one thing, don't use too many if/else statement, if it's me, I will change the code like this:
const handleOptionChange = (question, answer) => {
onChangeSelectionQuestionAnswer(question, answer)
}
const handleFreeAnswerChange = (value, question) => {
onChangeFreeTextQuestionAnswer(question, value)
}
const handleChangeFreeTextQuestionAnswer = (questionId, answer) => {
const targetAnswer = chosenAnswers.find(item => item.id === questionId)
let newAnswers = [...chosenAnswers]
if (!targetAnswer) {
newAnswers.push({ id: questionId, freeAnswer: answer })
}
if (targetAnswer) {
const idx = newAnswers.indexOf(targetAnswer)
newAnswers[idx].freeAnswer = answer
}
setChosenAnswers(newAnswers)
}
const handleChangeSelectionQuestionAnswer = (questionId, answerId) => {
const targetAnswer = chosenAnswers.find(item => item.id === questionId)
let newAnswers = [...chosenAnswers]
if (!targetAnswer) {
newAnswers.push({ id: questionId, answers: [answerId] })
}
if (targetAnswer) {
const idx = newAnswers.indexOf(targetAnswer)
newAnswers[idx].answers = [answerId]
}
setChosenAnswers(newAnswers)
}
You will find handleChangeFreeTextQuestionAnswer and handleChangeSelectionQuestionAnswer has duplicate code, so you can simplified further more
const handleChangeFreeTextQuestionAnswer = (questionId, answer) => {
handleChangeQuestionAnswer(questionId, answer, 'freeAnswer')
}
const handleChangeSelectionQuestionAnswer = (questionId, answerId) => {
handleChangeQuestionAnswer(questionId, [answer], 'answers')
}
const handleChangeQuestionAnswer = (questionId, newValue, valueField) => {
const targetAnswer = chosenAnswers.find(item => item.id === questionId)
let newAnswers = [...chosenAnswers]
if (!targetAnswer) {
newAnswers.push({ id: questionId, [valueField]: newValue })
}
if (targetAnswer) {
const idx = newAnswers.indexOf(targetAnswer)
newAnswers[idx][valueField]= newValue
}
setChosenAnswers(newAnswers)
}
If you want to add a new question type in the future, you only need to add a new handleChangeXXXQuestionAnswer function, then adjust the answer format and update field, and call handleChangeQuestionAnswer, you don't need to add more and more if/else or switch statement.
Since you are already have separate handler anyways, I suggest just passing the formatted answer object to your handleChangeQuestionAnswer. For example, you can change your dedicated question type handlers to the following
const handleOptionChange = ( id, answer) => {
onChangeQuestionAnswer({id, answers: [answer]})
}
const handleFreeAnswerChange = (freeAnswer, id) => {
onChangeQuestionAnswer({id, freeAnswer})
}
As for the general handler you can use an object instead of an array to keep track of the answers. With an object you can use the same spread syntax as you did with the array. And thanks to the other update above you can really simplify your general handler to one line. Please the updated function below
const [chosenAnswers, setChosenAnswers] = useState({})
const handleChangeQuestionAnswer = (question) => {
setChosenAnswers(answers => {...answers, ...{[question.id]: question}})
}
Note: I use a handler to update the state to avoid any race condition. AFAIK this is always the preferred way to update the state with the hook setter function.
When submitting the answers to the server use Object.values() to get values as an array. Ex:
Object.values(chosenAnswers)
The data and the handlers look clean. In the parent component, there is some logic repeated four times (returning the answer or freeAnswer keys and associated string or array). I would put that into a variable:
const answerForm =
type === 'oneCorrectAnswer'
? { answers: [answerId] }
: { freeAnswer: answerId };
Then spread that variable when you call setChosenAnswers or return objects when you map over chosenAnswers. Ex.
[...chosenAnswers, { id: questionId, ...answerForm }]
That also allows you to remove two of the if/elses because aside from that duplicate logic the conditions are the same.
You could also modify the foundedId if/else in two ways:
Reverse the order and remove negative conditional (considered not a best practice by some).
Change if/else to ternary - more deterministic, less room for side effects.
Set result to variable (answerToSubmit) and then call setChosenAnswsers once instead of twice with that variable
const answerToSubmit = foundedId
? chosenAnswers.map((item) => {
if (item.id !== questionId) return item;
return {
...item,
...answerForm,
};
})
: [...chosenAnswers, { id: questionId, ...answerForm }];
Full code:
const [chosenAnswers, setChosenAnswers] = useState([]);
const handleChangeQuestionAnswer = (questionId, answerId, type) => {
const foundedId = chosenAnswers.find((item) => item.id === questionId);
const answerForm =
type === 'oneCorrectAnswer'
? { answers: [answerId] }
: { freeAnswer: answerId };
const answerToSubmit = foundedId
? chosenAnswers.map((item) => {
if (item.id !== questionId) return item;
return {
...item,
...answerForm,
};
})
: [...chosenAnswers, { id: questionId, ...answerForm }];
setChosenAnswers(answerToSubmit);
};
If you had more answer types, I might suggest a switch statement, but overall this reduces duplicate logic and makes code more concise.

React - Check for duplicates in an array

I'm working on simple list where you can simply add your words to the list.
Main problem is duplicates, I tried many solutions but they weren't even close.
state = {
people: [{ name: null, count: null }]
}
handleSubmit = (e) => {
this.setState(({ count }) => ({
count: count + 1
}));
this.props.addHuman(this.state);
}
addHuman = (human) => {
let people = [...this.state.people, human];
this.setState({
people: people
});
}
I hope for solution which will check if there is any duplicate already in the array
You could make a check if there is someone with the same name already in the array. A better property to check would be an email adresse.
find takes a callback function as parameter. Inside this function, I compare the name properties. If it's a match, find returns true, then a do an early return in the next line and the person isn't added.
addHuman = (human) => {
const exists = this.state.people.find(p => p.name === human.name);
if (exists) return;
let people = [...this.state.people, human];
this.setState({
people: people
});
}
https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Array/find
if you want to de duplicate the array...you can do it in this way
cont array = [1,1.2,3,5,5];
[...new Set(array)]
This will work as well, there are plenty of ways to achieve the desired outcome.
addHuman = human => {
const updatedPeople = people.includes(human) ? people : [ ...people, human ];
this.setState({
people: updatedPeople
});
}
Remove_Duplicate_recs(filteredRecord)
{
var records = [];
for (let record of filteredRecord)
{
const duplicate_recorde_exists = records.find(r => r.MovementId ===
record.MovementId);
if (duplicate_recorde_exists)
{
continue ;
}
else
{
records.push(record);
}
}
return records;
}

Creating new array vs modifing the same array in react

Following is the piece of code which is working fine, but I have one doubt regarding - const _detail = detail; code inside a map method. Here you can see that I am iterating over an array and modifying the object and then setting it to setState().
Code Block -
checkInvoiceData = (isUploaded, data) => {
if (isUploaded) {
const { invoiceData } = this.state;
invoiceData.map(invoiceItem => {
if (invoiceItem.number === data.savedNumber) {
invoiceItem.details.map(detail => {
const _detail = detail;
if (_detail.tagNumber === data.tagNumber) {
_detail.id = data.id;
}
return _detail;
});
}
return invoiceItem;
});
state.invoiceData = invoiceData;
}
this.setState(state);
};
Is this approach ok in React world or I should do something like -
const modifiedInvoiceData = invoiceData.map(invoiceItem => {
......
code
......
})
this.setState({invoiceData: modifiedInvoiceData});
What is the pros and cons of each and which scenario do I need to keep in mind while taking either of one approach ?
You cannot mutate state, instead you can do something like this:
checkInvoiceData = (isUploaded, data) => {
if (isUploaded) {
this.setState({
invoiceData: this.state.invoiceData.map(
(invoiceItem) => {
if (invoiceItem.number === data.savedNumber) {
invoiceItem.details.map(
(detail) =>
detail.tagNumber === data.tagNumber
? { ...detail, id: data.id } //copy detail and set id on copy
: detail //no change, return detail
);
}
return invoiceItem;
}
),
});
}
};
Perhaps try something like this:
checkInvoiceData = (isUploaded, data) => {
// Return early
if (!isUploaded) return
const { invoiceData } = this.state;
const updatedInvoices = invoiceData.map(invoiceItem => {
if (invoiceItem.number !== data.savedNumber) return invoiceItem
const details = invoiceItem.details.map(detail => {
if (detail.tagNumber !== data.tagNumber) return detail
return { ...detail, id: data.id };
});
return { ...invoiceItem, details };
});
this.setState({ invoiceData: updatedInvoices });
};
First, I would suggest returning early rather than nesting conditionals.
Second, make sure you're not mutating state directly (eg no this.state = state).
Third, pass the part of state you want to mutate, not the whole state object, to setState.
Fourth, return a new instance of the object so the object reference updates so React can detect the change of values.
I'm not saying this is the best way to do what you want, but it should point you in a better direction.

Remove an Object from Array which is nested in an Object

I started a little bit playing with redux and i am amazed so far.
My problem right now is, that my new reducer function changes the type of one state variable and i dont want that.
The state shall have a form like that:
I only want to delete an object from a jsons array:
pseudo:
delete state.items[item_index].jsons[json_to_delete_index]
I ended up with this reducer, which is returning the item state now as an object and not as an array.
case DELETE_JSON:
const item_index = state.items.findIndex((url) => url.url_id === action.payload.url_id);
const json_index = state.items[item_index].jsons.findIndex((json) => json.json_id === action.payload.json_id);
return {
...state,
items: {
...state.items,
[item_index]: {
...state.items[item_index],
jsons:
[
...state.items[item_index].jsons.splice(0, json_index),
...state.items[item_index].jsons.splice(json_index + 1)
]
}
}
};
I tried various approaches so far, but changing states inside highly nested objects seems still like a torture with redux. Does anybody maybe know a way to write it?
Changing state with highly nested objects can be difficult but map and filter functions are really helpful in this case
const item_index = state.items.findIndex((url) => url.url_id === action.payload.url_id);
const json_index = state.items[item_index].jsons.findIndex((json) => json.json_id === action.payload.json_id);
return {
...state,
items: state.items.map((item, index) => (index === item_index ?
{ ...item, item.jsons.filter((json, i) => (i !== json_index)) } : item))
};
I solved it by using update() from immutability-helpers.
Very handy
import update from 'immutability-helper';
/* some other code */
case DELETE_JSON:
const item_index = state.items.findIndex((url) => url.url_id === action.payload.url_id);
const json_index = state.items[item_index].jsons.findIndex((json) => json.json_id === action.payload.json_id);
return update(state, {
items: {
[item_index]: {
jsons: {$splice: [[json_index]]}
}
}
});

Which approach in React is better?

Below both code does exactly same but in different way. There is an onChange event listener on an input component. In first approach I am shallow cloning the items from state then doing changes over it and once changes are done I am updating the items with clonedItems with changed property.
In second approach I didn't cloned and simply did changes on state items and then updated the state accordingly. Since directly (without setState) changing property of state doesn't call updating lifecycles in react, I feel second way is better as I am saving some overhead on cloning.
handleRateChange = (evnt: React.ChangeEvent<HTMLInputElement>) => {
const {
dataset: { type },
value,
} = evnt.target;
const { items } = this.state;
const clonedItems = Array.from(items);
clonedItems.map((ele: NetworkItem) => {
if (ele.nicType === type) {
ele.rate = Number(value);
}
});
this.setState({ items: clonedItems });
};
OR
handleRateChange = (evnt: React.ChangeEvent<HTMLInputElement>) => {
const {
dataset: { type },
value,
} = evnt.target;
const { items } = this.state;
items.map((ele: NetworkItem) => {
if (ele.nicType === type) {
ele.rate = Number(value);
}
});
this.setState({ items });
};
You can use this
this.setState(state => {
const list = state.list.map(item => item + 1);
return {
list,
};
});
if you need more info about using arrays on states, please read this: How to manage React State with Arrays
Modifying the input is generally a bad practice, however cloning in the first example is a bit of an overkill. You don't really need to clone the array to achieve immutability, how about something like that:
handleRateChange = (evnt: React.ChangeEvent<HTMLInputElement>) => {
const {
dataset: { type },
value,
} = evnt.target;
const { items } = this.state;
const processedItems = items.map((ele: NetworkItem) => {
if (ele.nicType === type) {
return {
...ele,
rate: Number(value)
};
} else {
return ele;
}
});
this.setState({ items: processedItems });
};
It can be refactored of course, I left it like this to better illustrate the idea. Which is, instead of cloning the items before mapping, or modifying its content, you can return a new object from the map's callback and assign the result to a new variable.

Categories

Resources