redux: IDs vs objects - javascript

Redux documentation gives an example of storage that keeps posts and comments. To reduce complicity of nested objects it suggests to keep comments as array of IDs.
So the code of posts reducer will look like this:
function postsById(state = {}, action) {
switch (action.type) {
case 'ADD_COMMENT':
var { commentId, postId, commentText } = action.payload;
var post = state[postId];
return {
...state,
[postId]: {
...post,
comments: post.comments.concat(commentId)
}
}
default:
return state
}
}
But why not keep the whole objects there:
...
return {
...state,
[postId]: {
...post,
comments: post.comments.concat(
{commentId, commentText} // <=
)
}
}
...
If we do so we don't need to use complex selectors and computations to get required data:
// keeping IDs
function getPostComments(state, postId) {
return state.postsById[postId].comments.map(
commentId => state.commentsById[commentId]
)
}
// keeping objects
function getPostComments(state, postId) {
return state.postsById[postId].comments
}
Though this example is very simple, in other cases keeping the whole objects will make complex selectors much easier.

Related

Issue with updating a property that is an array nested inside another array in redux state

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!

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.

Redux action creator syntax

function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text
}
dispatch(action)
}
http://redux.js.org/docs/basics/Actions.html#action-creators
I saw the code above from redux documentation. What I don't understand is the text in the action. It doesn't look like a valid javascript object or array syntax. Is it an ES6 new syntax? Thanks.
It is just a new ES6 syntax, which simplifies creating properties on the literal syntax
In short, if the name is the same as the property, you only have to write it once
So this would be exactly the same :)
function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text: text
}
dispatch(action)
}
In the above code
function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text
}
dispatch(action)
}
here text is an example of object literal shorthand notation. ES6 gives you a shortcut for defining properties on an object whose value is equal to another variable with the same key.
As has been said this is just shorthand for writing
const action = {
type: ADD_TODO,
text: text
}
dispatch(action)
Have a look at this blog
If you look at the next page in document http://redux.js.org/docs/basics/Reducers.html
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
})
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: state.todos.map((todo, index) => {
if(index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
})
default:
return state
}
}
It is expecting property name text. As #Icepickle mentioned it is a valid format but you can also change to below format:
function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text:text
}
dispatch(action)
}

Reducer cannot read property 'photos' of undefined? What am I doing wrong?

Here is the initial state of my reducer, and I need to set it up in this way due to some post processing I need to do:
const initialState = {
showAll: {
photos: null
}
}
Basically, I have a page where you see all your photos, and you can tag certain ones as your pinned photos.
Here's part of my reducer logic:
if (state.showAll.photos) {
const showAllState = state.showAll.photos;
showAllState.map(m => {
if (action.payload.id === m.id) {
m.pinned = true;
}
});
showAllAfterPin = showAllState;
} else {
showAllAfterPin = state.showAll.photos;
}
However, I get an error saying cannot read property 'photos' of undefined and I'm not sure what I am doing wrong.
Might be easier to just set your photos in initialState to empty array [] instead of null.
Another thing, your reducer should not mutate your state object.
Doing const showAllState = state.showAll.photos doesn't make it a new object.
Last thing, showAllState.map(...) needs to return an item inside the function body. It will create a new array.
Here's something you can do...
const { photos = [] } = state.showAll;
const updatedPhotos = photos.map(photo => {
if (action.payload.id === photo.id) {
return Object.assign({}, photo, { pinned: true })
}
return photo;
});
// return entire state if this is inside your root reducer
return {
...state,
showAll {
...state.showAll,
photos: updatedPhotos
}
}

Changing a nested array value based on a given index in a reducer while not mutating state

I have a reducer working with an object with arrays. I want to change a value on the nested arrays based on a given index. This code works but I can't seem to get my test to work using deep freeze. I was trying to look at the redux example here http://redux.js.org/docs/basics/Reducers.html using .map to find the index with no luck. Any ideas?
export default function myReducer(state = { toDisplay: [] }, action) {
const { type, groupIndex, itemIndex } = action;
const newObject = Object.assign({}, state);
switch (type) {
case actionTypes.TOGGLE_GROUP:
newObject.toDisplay[groupIndex].isSelected = newObject.toDisplay[groupIndex].isSelected ? false : 'selected';
return newObject;
case actionTypes.TOGGLE_ITEM:
newObject.toDisplay[groupIndex].values[itemIndex].isSelected = newObject.toDisplay[groupIndex].values[itemIndex].isSelected ? false : true;
return newObject;
default:
return state;
}
}
EDIT:
For anyone curious after watching a helpful redux video I came up with this:
export default function myReducer(state = { toDisplay: [] }, action) {
const { type, groupIndex, itemIndex } = action;
switch (type) {
case actionTypes.TOGGLE_GROUP:
return {
...state,
toDisplay: [
...state.toDisplay.slice(0, groupIndex),
{
...state.toDisplay[groupIndex],
isSelected: state.toDisplay[groupIndex].isSelected ? false : 'selected'
},
...state.toDisplay.slice(groupIndex + 1)
]
};
case actionTypes.TOGGLE_ITEM:
return {
...state,
toDisplay: [
...state.toDisplay.slice(0, groupIndex),
{
...state.toDisplay[groupIndex],
values: [
...state.toDisplay[groupIndex].values.slice(0, itemIndex),
{
...state.toDisplay[groupIndex].values[itemIndex],
isSelected: state.toDisplay[groupIndex].values[itemIndex].isSelected ? false : true
},
...state.toDisplay[groupIndex].values.slice(itemIndex + 1)
]
},
...state.toDisplay.slice(groupIndex + 1)
]
};
default:
return state;
}
}
Using a helper/library like suggested is probably the best route but my team wishes to not add another dependency.
Firstly, Object.assign(...) only does a shallow copy. See here.
As you have objects nested inside arrays nested inside objects I would highly recommend the immutability helpers from react (as mentioned by Rafael). These allow you to do something like this:
case actionTypes.TOGGLE_GROUP:
return update(state, {
toDisplay: {
[groupIndex]: {
isSelected: {$set: newObject.toDisplay[groupIndex].isSelected ? false : 'selected'}
}
}
});
If you are looking to modify a simple value inside an array with raw js then you can use something like this:
return list
.slice(0,index)
.concat([list[index] + 1])
.concat(list.slice(index + 1));
(source)

Categories

Resources