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
}
}
Related
I'm writing this react component to render all chats of an user in a chat app.
The conversation list is acquired from a REST end point and set as a state variable.
When a new message arrives through socket, I'm trying to bring the conversation in the list to the top and mark it in a different color.
The code for that looks like the following:
const [conversationsList, setConversationsList] = useState([]);
//When a new message arrives,
const markNewConversation = (message) => {
console.log(conversationsList); //Empty array
let newConversationList = conversationsList.map((conversationElement) => {
if (conversationElement.thread_id === message.thread_id) {
conversationElement.date_created = message.date_created;
conversationElement.new_for.push(user._id);
}
return conversationElement;
});
console.log(newConversationList);
newConversationList = newConversationList.sortBy(function (o) {
return o.date_created;
});
console.log(newConversationList); //Empty as well.
setConversationsList(newConversationList); //Whole screen goes black on this.
};
useEffect(() => {
if (user._id === null) history.push("/");
connectSocket();
socket.on("_messageIn", markNewConversation);
getThreads().then((threads) => {
threads.forEach((thread, index) => {
let allParticipants = thread.thread_participants;
threads[index].other_user = allParticipants.find((participant) => {
return participant._id != user._id;
});
});
setConversationsList(threads);
setIsLoading(false);
});
// returned function will be called on component unmount
return () => {
socket.off("_messageIn", markNewConversation);
};
}, []);
return conversationsList.map((conversation) => {
return(//magically appears inside this div.)
})
The problem is when a new message arrives, the function receives an empty array and the entire screen becomes empty. I'm not even setting the array to empty anywhere. Where am I going wrong?
In this function you're mutating objects, this can cause weird errors.
let newConversationList = conversationsList.map((conversationElement) => {
if (conversationElement.thread_id === message.thread_id) {
conversationElement.date_created = message.date_created;
conversationElement.new_for.push(user._id);
}
return conversationElement;
});
it should look like this:
let newConversationList = conversationsList.map((conversationElement) =>
(conversationElement.thread_id === message.thread_id) ? {
...conversationElement,
date_created: message.date_created,
new_for: [...conversationElement.new_for, user.id]
} : {
...conversationElement,
new_for: [...conversationElement.new_for]
}
There is a concept in javascript of State Mutating and we should never mutate the objects directly. Here you are making the mistake is that when new message comes it replaces whole object with new one and kind of re-initialize it. So spread the state using spread operator before adding new message.
Do like this:
let newConversationList = conversationsList.map((conversationElement) =>
(conversationElement.thread_id === message.thread_id) ? {
...conversationElement, //spread/copy like this before mutating
date_created: message.date_created,
new_for: [...conversationElement.new_for, user.id]
} : {
...conversationElement, //spread/copy like this before mutating
new_for: [...conversationElement.new_for]
}
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 fetching recipes from a recipe app and id like to insert certain objects from the returning json result onto my state with setstate. I know how to do one of these but im having trouble figuring out how to map the results on to my state. Can anyone help me on this?
The code for the issue is here. I have changed my api key and code for security
componentDidMount() {
let url = `https://api.edamam.com/search?q=banana&app_id=chjhvje1&app_key=b67djhhvhvhaef`;
fetch(url)
.then((response) => {
return response.json();
})
.then((data) => {
let recipeUIState = [ ...this.state.RecipeUI ];
recipeUIState[0].title = data.hits[0].recipe.label;
recipeUIState[0].thumbnail = data.hits[0].recipe.image;
recipeUIState[0].href = data.hits[0].recipe.url;
this.setState({ RecipeUI: recipeUIState });
console.log(data.hits[0].recipe);
});
}
State is as follows-
export default class RecipeUI extends Component {
constructor(props) {
super(props);
this.state = {
food: '',
RecipeUI: [ { title: '' } ]
// thumbnail: '', ingredients: '', href: ''
};
this.search = this.search.bind(this);
}
reponse from API is attached as image
data.hits.forEach(({ recipe }) => {
// We get the original state every before it's updated in the iteration
const recipeUIState = [...this.state.RecipeUI];
// Check if there's an existing recipe with the same title
const idx = recipeUIState.findIndex(r => r.title === recipe.title);
// Helper object to create a recipe from the current iteration
const currentRecipe = {
title: recipe.label,
thumbnail: recipe.image,
href: recipe.url
};
// `findIndex` returns -1 if no entry was found, otherwise it returns the index
if (idx < 0) {
// No existing recipe was found, append the new recipe to the original state
return this.setState({
recipeUIState: [...recipeUIState, ...currentRecipe]
});
}
// Recipe already existed, create a new recipe by overwriting
// the object at the index we found earlier
const newRecipeUIState = {
...recipeUIState[idx],
...currentRecipe
};
// Replace the recipe at found index
recipeUIState[idx] = newRecipeUIState;
this.setState({ recipeUIState });
});
Something like this? could probably be simplified using Array#reduce but I don't feel too comfortable using it.
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.
I'm trying to return an array from the reducer after filling it with data, but it always returns an empty array.
export default function dashboards(state: any = dashboardsState, action: any) {
// more code
if (action.type === Types.LOAD_DASHBOARD_SUCCESS) {
// create the cards holder in the store
var cardsSkull: any = [];
Object.keys(action.payload.lists).map((idLis: any) => {
cardsSkull[idLis] = {};
console.log("still up");
});
action.payload.importedDashboards.map((impDash: any) => {
Object.keys(impDash.mappedLists).map((key: any) => {
const mpList = impDash.mappedLists[key];
cardsSkull[mpList.idlistLocal][impDash.remote_board_id] = [1, 2, 3];
console.log("still working");
});
});
console.log(cardsSkull);
// assign and ready to go
return Object.assign({}, state, {
selectedDashboardData: action.payload,
cards: cardsSkull
});
}
// more code
return state;
}
when I console.log(cardsSkull) everything looks right.
I expect the cards to have the value of cardsSkull after it is filled, but the actual output is an empty array
You define cardsSkull as array (which is an object under the hood), but because idLis is not a number, cardsSkull[idLis] = {}; does not populate an element within the array part of the object, but rather an element within the array object itself. Therefore in your screenshot, the length property of your array is still 0! Use numbers for idLis and your problem is solved.
it seems that you are assuming wrong type for cards, it is an object instead of array. I would suggest to update as following
export default function dashboards(state: any = dashboardsState, action: any) {
// more code
if (action.type === Types.LOAD_DASHBOARD_SUCCESS) {
// create the cards holder in the store
const cardsSkull = Object.keys(action.payload.lists).reduce(
(acc, idLis) => ({ ...acc, [idList]: {} }),
{}
);
action.payload.importedDashboards.forEach((impDash: any) => {
Object.keys(impDash.mappedLists).forEach((key: any) => {
const mpList = impDash.mappedLists[key];
cardsSkull[mpList.idlistLocal][impDash.remote_board_id] = [1, 2, 3];
console.log('still working');
});
});
console.log(cardsSkull);
// assign and ready to go
return {
...state,
selectedDashboardData: action.payload,
cards: cardsSkull
}
}
// more code
return state;
}