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.
Related
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
),
},
};
});
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!
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;
}
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.
I am trying to provide functionality in my webpage for editing state data.
Here is the state structure
state = {
eventList:[
{
name: "Coachella"
list: [
{
id: 1,
name: "Eminem"
type: "rap"
}
{
id: 2,
name: "Kendrick Lamar"
type: "rap"
}
]
}
]
}
I want to be able to edit the list arrays specifically the id, name, and type properties but my function doesn't seem to edit them? I currently pass data I want to override id name and type with in variable eventData and an id value specifying which row is selected in the table which outputs the state data.
Here is the function code:
editPickEvent = (eventData, id) => {
const eventListNew = this.state.eventList;
eventListNew.map((event) => {
event.list.map((single) => {
if (single.id == id) {
single = eventData;
}
});
});
this.setState({
eventList: eventListNew,
});
};
When I run the code the function doesn't alter the single map variable and I can't seem to pinpoint the reason why. Any help would be great
edit:
Implementing Captain Mhmdrz_A's solution
editPickEvent = (eventData, id) => {
const eventListNew = this.state.eventList.map((event) => {
event.list.map((single) => {
if (single.id == id) {
single = eventData;
}
});
});
this.setState({
eventList: eventListNew,
});
};
I get a new error saying Cannot read property list of undefined in another file that uses the map function to render the state data to the table?
This is the part of the other file causing the error:
render() {
const EventsList = this.props.eventList.map((event) => {
return event.list.map((single) => {
return (
map() return a new array every time, but you are not assigning it to anything;
editPickEvent = (eventData, id) => {
const eventListNew = this.state.eventList.map((event) => {
event.list.forEach((single) => {
if (single.id == id) {
single = eventData;
}
});
return event
});
this.setState({
eventList: eventListNew,
});
};
const editPickEvent = (eventData, id) => {
const updatedEventList = this.state.eventList.map(event => {
const updatedList = event.list.map(single => {
if (single.id === id) {
return eventData;
}
return single;
});
return {...event, list: updatedList};
});
this.setState({
eventList: updatedEventList,
});
}
Example Link: https://codesandbox.io/s/crazy-lake-2q6ez
Note: You may need to add more checks in between for handling cases when values could be null or undefined.
Also, it would be good if you can add something similar to the original data source or an example link.
Turns out primitive values are pass by value in javascript, which I didn't know and why the assignment wasn't working in some of the previous suggested answers. Here is the code that got it working for me:
editEvent = (EventData, id) => {
const eventListNew = this.state.eventList.map((event) => {
const newList = event.list.map((single) => {
return single.id == id ? EventData : single;
});
return { ...event, list: newList };
});
this.setState({
eventList: eventListNew,
});
};