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]]}
}
}
});
Related
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 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.
So I've been browsing through the issues regarding redux state on here and can't seem to find one that helps me. My issue is that I have state that looks like this:
state = {
products = [
{
name: "Example",
description: "Example",
images: ['img1.jpg', 'img2.jpg'],
imgNum: 2,
}
]
}
And I would like to add to images and imgNum like this:
state = {
products = [
{
name: "Example",
description: "Example",
images: ['img1.jpg', 'img2.jpg', 'img3.jpg'],
imgNum: 3,
}
]
}
(This is an example, the actual code has more properties and items but images and imgNum are the items of concern)
So when I try to update the images array or imgNum I am unable to. Here is what the case looks like:
case ADD_IMAGE:
return {
...state,
products: state.products.map((item, i) => {
if(item.name === payload.name) {
return {
...item,
images: [...item.images, payload.image],
imgNum: item.imgNum + 1,
}
}
return item;
});
}
I've checked to make sure that in that scope item.images, ...item.images, and item.imgNum are all defined and they all return values when console.log'ed before the return in the if statement. I've also tried different values in place of ...item.images but the only other one that doesn't throw an undefined error is
...state.products[i].images
and it also didn't result in any state changes.
Just a clarification - with my code as it is now I don't get any errors, but the state also is not updated.
payload.name and payload.image also return values and not undefined, as per the ol console.log test again.
Another thing I tried was declaring variables outside of the return statement and then using them inside like this:
case ADD_IMAGE:
return {
...state,
products: state.products.map((item, i) => {
if(item.name === payload.name) {
const newArr = [...item.images, payload.image];
const newNum = item.imgNum +1;
return {
...item,
images: newArr,
imgNum: newNum
}
}
return item;
});
}
But this brilliant bit of coding resulted in nothing.
Any help is appreciated, I'm sure I've overlooked (or completely blown by) some nuance of updating state.
Let me know if additional info is needed! Thanks in advance all.
Edit to show action:
export const uploadImage = (data, image, item) => (dispatch) => {
axios
.post("/products/upload-image", data)
.then(() => {
dispatch({type: ADD_IMAGE, payload: {image: image, name: item.name}})
})
.catch((err) => console.log(err));
};
So this is the action I use, and it is called on a file upload event. I know it works because the image is uploaded to my db (the axios part), and because redux-logger shows the ADD_IMAGE action occurring on file upload. The payload image and name are as expected, so that stuff is all working.
instead of this:
products: state.products.map((item, i) => {
if(item.name === payload.name) {
const newArr = [...item.images, payload.image];
const newNum = item.imgNum +1;
return {
...item,
images: newArr,
imgNum: newNum
}
}
return item;
});
try this:
return {
...state,
products: state.products.map((item, i) =>
item.name === payload.name ? {
...item,
images: item.images.concat(payload.image),
imgNum: item.imgNum +1
} : item,
)
}
Thanks to those that put forward suggestions. I came up with a workaround I'll stick here.
Instead of fiddling around with the reducer stuff anymore, I changed what I was sending when the action fired. I handled getting the current image array from the store, storing it as a new variable, pushing my new image onto that new variable array, and then providing the new variable to my action, which updates the image array as a whole. Here are some snippets in case that isn't super clear:
case ADD_IMAGE:
return {
...state,
products: state.products.map((item) => {
if (item.name === payload.name) {
return {
...item,
images: payload.imageArr
};
}
return item;
}),
};
note that image just is assigned payload.imageArr now.
In my component, I did this:
const imageName = `${Math.round(Math.random() * 100000000).toString()}.jpg`;
const oldArr = currentItem.images;
oldArr.push(imageName);
where currentItem is an object in my store that is set by an event handler earlier on, and has all the same props as any other product in the products array. imageName is just that random string that I assign the image, and then I just push it onto the array. Then that 'oldArr' (excuse the lame naming, but wanted it to be clear) is passed when the action is called, and assigned as payload.imageArr as mentioned above.
Thanks Red Baron, Hemant, and Ceva Comic for your input on the issue!
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.
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.