React state update not working with setState - javascript

Okay so it's simple
I have an array of answers inside an array of questions.
the user has the option to select more than one answer.
When an answer is selected, the text should change to selected and unselected if it isn't selected.
These are the steps i've tried to update my state
step 1 using map
setTestInfo((state) => {
const allStateQuestions = state.info.questions;
const currentQuestion = allStateQuestions.filter(
(question) => question.id === questionId
)[0];
const allAnswersMap = currentQuestion.answers.map((answer) =>
answer.id === answerId
? (() => {
answer.is_chosen = !answer.is_chosen;
return answer;
})()
: answer
);
currentQuestion.answers = allAnswersMap;
return {
...state,
info: {
...state.info,
questions: allStateQuestions,
},
};
});
step 2 using find
setTestInfo((state) => {
const allStateQuestions = state.info.questions;
const currentQuestion = allStateQuestions.filter(
(question) => question.id === questionId
)[0];
const currentAnswer = currentQuestion.answers.find(
(answer) => answer.id === parseInt(answerId)
);
currentAnswer.is_chosen = !currentAnswer.is_chosen;
// i even went to the extend of reassigning it yet it doesn't work
currentQuestion.answers.filter((answer) => answer.id === answerId)[0] =
currentAnswer;
return {
...state,
info: {
...state.info,
questions: allStateQuestions,
},
};
});
Well after using the sample logics above, none of them seem to work
Thanks in advance

Issue
You are mutating the state in both cases. I'll cover the first snippet.
setTestInfo((state) => {
const allStateQuestions = state.info.questions; // <-- reference to state
const currentQuestion = allStateQuestions.filter( // <-- reference to state
(question) => question.id === questionId
)[0];
const allAnswersMap = currentQuestion.answers.map((answer) =>
answer.id === answerId
? (() => {
answer.is_chosen = !answer.is_chosen; // <-- state mutation!!
return answer;
})()
: answer
);
currentQuestion.answers = allAnswersMap; // <-- state mutation!!
return {
...state,
info: {
...state.info,
questions: allStateQuestions, // <-- saved reference back into state
},
};
});
The currentQuestion.answers object of the state.info.questions state was mutated and the state.info.questions array reference never changed so React isn't "seeing" this as an update and isn't triggering a rerender.
Solution
Apply the immutable update pattern. You must shallow copy all updates into new array and object references.
setTestInfo((state) => {
return {
...state,
info: {
...state.info,
// new questions array
questions: state.info.questions.map(question => question.id === questionId
? { // new question object
...question,
// new answers array
answers: question.answers.map(answer => answer.id === answerId
? { // new answer object
...answer,
is_chosen: !answer.is_chosen,
}
: answer
),
}
: question
),
},
};
});

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.

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]]}
}
}
});

Do I need to use PrevState even if I spread the state into a variable?

I am testing some code to try and understand the race condition regarding the use of setState().
my code can be found here
my code below:
import React from "react";
export default class App extends React.Component {
state = {
id: "",
ids: [{ id: 7 }, { id: 14 }]
};
// here is where I create the id numbers
uniqueIdCreatorHandler = incrementAmount => {
let ids = [...this.state.ids];
let highestId = 0;
if (ids.length > 0) {
highestId = ids
.map(value => {
return value.id;
})
.reduce((a, b) => {
return Math.max(a, b);
});
}
let newId = highestId + incrementAmount;
ids.push({ id: newId });
this.setState({ ids: ids });
};
idDeleterHanlder = currentIndex => {
let ids = this.state.ids;
ids.splice(currentIndex, 1);
this.setState({ ids: ids });
};
//below is when I test performing the function twice, in order to figure if the result would be a race condition
double = (firstIncrementAmount, secondIncrementAmount) => {
this.uniqueIdCreatorHandler(firstIncrementAmount);
this.uniqueIdCreatorHandler(secondIncrementAmount);
};
render() {
let ids = this.state.ids.map((id, index) => {
return (
<p onClick={() => this.idDeleterHanlder(index)} key={id.id}>
id:{id.id}
</p>
);
});
return (
<div className="App">
<button onClick={() => this.uniqueIdCreatorHandler(1)}>
Push new id
</button>
<button onClick={() => this.double(1, 2)}>Add some Ids</button>
<p>all ids below:</p>
{ids}
</div>
);
}
}
when invoking the double function on the second button only the secondIncrementAmount works. You can test it by changing the argument values on the call made on the onClick method.
I think that I should somehow use prevState on this.setState in order to fix this.
How could I avoid this issue here? This matter started at CodeReview but I did not realize how could I fix this.
There is also a recommendation to spread the mapped ids into Math.max and I could not figure out how and Why to do it. Isn't the creation of the new array by mapping the spreaded key values safe enough?
.splice and .push mutate the array. Thus the current state then does not match the currently rendered version anymore. Instead, use .slice (or .filter) and [...old, new] for immutable stateupdates:
deleteId = index => {
this.setState(({ ids }) => ({ ids: ids.filter((id, i) => i !== index) }));
};
uniqueIdCreatorHandler = increment => {
const highest = Math.max(0, ...this.state.ids.map(it => it.id ));
this.setState(({ ids }) => ({ ids: [...ids, { id: highest + increment }] }));
};
setState can be asynchronous, batching up multiple changes and then applying them all at once. So when you spread the state you might be spreading an old version of the state and throwing out a change that should have happened.
The function version of setState avoids this. React guarantees that you will be passed in the most recent state, even if there's some other state update that you didn't know about. And then you can product the new state based on that.
There is also a recommendation to spread the mapped ids into Math.max and I could not figure out how and Why to do it
That's just to simplify the code for finding the max. Math.max can be passed an abitrary number of arguments, rather than just two at a time, so you don't need to use reduce to get the maximum of an array.
uniqueIdCreatorHandler = incrementAmount => {
this.setState(prevState => {
let ids = [...prevState.ids];
let highestId = Math.max(...ids.map(value => value.id));
let newId = highestId + incrementAmount;
ids.push({ id: newId });
this.setState({ ids: ids });
});
};
This isn't the most elegant solution but you can pass a callback to setState(see https://reactjs.org/docs/react-component.html#setstate).
If you modify uniqueIdCreatorHandler like this:
uniqueIdCreatorHandler = (incrementAmount, next) => {
let ids = [...this.state.ids];
let highestId = 0;
if (ids.length > 0) {
highestId = ids
.map(value => {
return value.id;
})
.reduce((a, b) => {
return Math.max(a, b);
});
}
let newId = highestId + incrementAmount;
ids.push({ id: newId });
this.setState({ ids: ids }, next); //next will be called once the setState is finished
};
You can call it inside double like this.
double = (firstIncrementAmount, secondIncrementAmount) => {
this.uniqueIdCreatorHandler(
firstIncrementAmount,
() => this.uniqueIdCreatorHandler(secondIncrementAmount)
);
};

How do I update an array of objects in component state?

I am trying to update the property of an object which is stored in an array.
my state looks something like this:
state = {
todos: [
{
id: '1',
title: 'first item,
completed: false
},
{
id: '2',
title: 'second item,
completed: false
}
],
}
What I am trying to do is access the second element in the 'todos' array and update the completed property to either false -> true or true -> false.
I have a button with the handler for update, and my class method for the update looks like this:
onUpdate = (id) => {
const { todos } = this.state;
let i = todos.findIndex(todo => todo.id === id);
let status = todos[i].completed
let updatedTodo = {
...todos[i],
completed: !status
}
this.setState({
todos: [
...todos.slice(0, i),
updatedTodo,
...todos.slice(i + 1)
]
});
}
While this does work, I want to find out if there is a more concise way of achieving the same result; I tried to use Object.assign(), but that didn't work out because my 'todos' is an array, not an object. Please enlighten me with better code!
It would be best to use update function to make sure you don't work on outdated data:
onUpdate = (id) => {
this.setState(prevState => {
const copy = [...prevState.todos];
const index = copy.findIndex(t => t.id === id);
copy[index].completed = !copy[index].completed;
return { todos: copy }
})
}
You can simply copy your todos from state, then make edits, and after that put it back to the state
onUpdate = (id) => {
var todos = [...this.state.todos]
var target = todos.find(todo => todo.id == id)
if (target) {
target.completed = !target.completed
this.setState({ todos })
}
}

Categories

Resources