Appending multi dimensional array in react state - javascript

I'm trying to append array which is react state:
const [ products, setProducts ] = useState([])
useEffect(() => {
config.categories.forEach(category => {
service.getCategory(category.name).then(data => {
const copy = JSON.parse(JSON.stringify(products))
copy[category.id] = data
setProducts(copy)
})
})
},[])
service.getCategory() fetches data over HTTP returning array. products is nested array, or at least it's suppose to be. config.category is defined as:
categories: [
{
name: 'product1',
id: 0
},
{
name: 'product2',
id: 1
},
{
name: 'product3',
id: 2
}]
}
Eventually products should be appended 3 times and it should contain 3 arrays containing products from these categories. Instead products array ends up including only data from last HTTP fetch, meaning the final array looks something like this
products = [null, null, [{},{},{},..{}]].
I hope someone knows what's going on? Been tinkering with this for a while now.

The problem is that your fulfillment handlers close over a stale copy of products (the empty array that's part of the initial state). In a useEffect (or useCallback or useMemo, etc.) hook, you can't use any state items that aren't part of the dependency array that you provide to the hook. In your case, you just want to get the data on mount, so an empty dependency array is correct. That means you can't use any state items in the callback.
What you can do instead is use the callback form of the state setter:
const [ products, setProducts ] = useState([]);
useEffect(() => {
config.categories.forEach(category => {
service.getCategory(category.name).then(data => {
setProducts(products => { // Use the callback form
const copy = products.slice(); // Shallow copy of array
copy[category.id] = data; // Set this data
return copy; // Return the shallow copy
});
});
});
}, []);
Or more concisely (but harder to debug!) without the explanatory comments:
const [ products, setProducts ] = useState([]);
useEffect(() => {
config.categories.forEach(category => {
service.getCategory(category.name).then(data => {
setProducts(products => Object.assign([], products, {[category.id]: data}));
});
});
}, []);
Those both use the same logic as your original code, but update the array correctly. (They also only make a shallow copy of the array. There's no need for a deep copy, we're not modifying any of the objects, just the array itself.)
But, that does a state update each time getCategory completes — so, three times in your example of three categories. If it happens that the request for id 2 completes before the request for id 1 or 0, your array will look like this after the first state update:
[undefined, undefined, {/*data for id = 2*/}]
You'll need to be sure that you handle those undefined entries when rendering your component.

Related

React updating a state array that is nested within array of objects

So I'm feeling somewhat confident on updating the array of objects when it's just array of objects using map and the spread function. However, where I'm stuck on is updating an array that is within array of objects.
Here's the example.
I have a seperate state to choose which index
const [option, setOption] = useState(0);
I initialize the state with array of objects
const [parts, setParts] = useState([
{bodyPart: "upperLip", price: 1000, active: [false, false, false]},
{bodyPart: "chin", price: 1000, active: [false, false, false]},
])
Basically, I want to update the array that is nested in array of objects when the description of bodyPart matches up.
const handleOnClick = (bodyPart) => {
parts.map((part) => {
if (part.bodyPart === bodyPart){
return {...part, active[option]=true}
} else {
//do nothing
}
})
}
I know that return {...part, active[option]=true} part is incorrect. What would be the proper way of updating this piece within the state?
I've tried to set it to true like I would for an array, but I'm not too sure anymore.

How to combine the received data in a state?

When I execute an API request, I get a lot of objects that are not connected by a common array. I'm trying to combine them into one state, but only one value is stored. How can I store all the received objects in the general array of my state? Everything would be fine, but I need the array to be stored in the state, in which all objects from the API will lie, in order to transfer this array to Redax
const [planets, setPlanets] = useState([]);
useEffect(() => {
(async () => {
const users = await axios(world);
const worldData = users.data;
setPlanets(worldData);
})();
}, [nextTeam, world]);
and i take get data
I am trying to transfer all these objects to state planets, but objects are transferred there individually. Not creating an array.
This is a screenshot of console.log(planets)
You can use Object.keys
let planets = {
tatooine:{
population: "1,3M", type: "desert"
},
coruscant:{
population: "130B", type: "city"
},
}
let array = []
Object.keys(planets).forEach(planet => array.push({
name: planet,
...planets[planet]
}))
console.log(array)

Getting error of fetched items after refreshing the page

I am fetching my data from external API as usual and this is the typical way I do it:
Fetch API:
const [tshirts, setTshirts] = useState([]);
const fetchData = () => {
fetch('apiEndpoint')
.then((response) => response.json())
.then((data) => {
setTshirts(data[0].clothes.regular.top); // path to my array
})
.catch((error) => {
console.log(error);
});
};
React.useEffect(() => {
fetchData();
}, []);
Map through an array:
const tshirtArray = tshirts.tShirt; // specifying the path
const listItems = tshirtArray.map((item) => <li>{item}</li>);
<ul>{listItems}</ul>
Example of data structure:
[
{
id: 1,
clothes: {
regular: {
top: {
sleeveless: [],
tShirt: [
"image-path-here"
],
.....
.....
.....
When I first time execute the code it works, but after some time or after refreshing the page I get an error of TypeError: Cannot read properties of undefined (reading 'map')
Why is that undefined? The path is correct and fetching the array should be as well. Can not find the reason of it not working.
I don't have reputation to comment, so let me try to clarify it for you through an answer. As #sojin mentioned, you cannot use tshirts.Tshirt since your state is of array type and arrays can't be used like objects, meaning that if there was an object of lets say exampleObject = { type: "shirt", color: "white } you could call it with exampleObject.type. Since you have an array of objects in your state (top that you are saving to state is still object which contains tShirt array), you first have to use index (to tell which object you want to use from the state array) and then you can use it like you wanted. For example, in your example there are 1 objects in state array. Array indexes start at 0. So you could do tshirts[0].tShirt to get the tShirt array from that object.
However, I would edit your code a bit. Instead of using tshirtArray constant, just do listItems from your state:
const listItems = tshirts.map((item) => {item.tShirt[0]});
Note: I've just used index 0 here to demonstrate the finding of the first item in tShirt array. If you want to see all tShirt image paths, then you may need to do nested mapping or other similar solutions.

Why can't I use dot notation on React State?

I'm creating a flashcard app and I'm currently trying to set the front side of the flashcard to some text from an API.
My state:
const [deckWithCards, setDeckWithCards] = useState([]);
deckWithCards is a deck of flashcards and it looks like:
{name: 'Test Name', description: 'Test Description', id: 3, cards: Array(4)};
When I do deckWithCards.cards I get:
[{id: 1, front: 'Front of card', back: 'Back of Card', deckId: 1}]
If I was to have 4 cards in a deck, I'll get an array with 4 of these objects with the corresponding data.
I need access to all of this information however, when I try to do deckWithCards.cards.front, I get "Cannot read property 'front' of undefined."
I also tried looping through the cards array like:
let arr = [];
let allCards = deckWithCards.cards;
for (let i = 0; i < allCards.length; i++) {
arr.push(allCards.front);
}
This gave me: "Cannot read property 'length' of undefined."
How do I gain access to the items in this cards array?
Helper functions:
export async function readDeck(deckId, signal) {
const url = `${API_BASE_URL}/decks/${deckId}?_embed=cards`;
return await fetchJson(url, { signal });
}
export async function listCards(deckId, signal) {
const url = `${API_BASE_URL}/cards?deckId=${deckId}`;
return await fetchJson(url, { signal });
}
How I set my State:
useEffect(() => {
const abortController = new AbortController();
readDeck(deckId, abortController.signal)
.then(setDeckWithCards)
.catch(setError)
listCards(deckId, abortController.signal)
.then(setCards)
.catch(error)
return () => abortController.abort();
}, []);
There is a moment in time while your useEffect and your fetch are still running before you set the cards. During that time, the value of deckWithCards is going to be the initial value that you provided in your useState. Your component has to be built in a way where it can run properly and render properly with that initial value. If the eventual value of the resolved deck is an object, then it makes no sense that your initial value is an empty array.
const [deckWithCards, setDeckWithCards] = useState([]);
I recommend that you set the initial state to null or undefined. Before you access any properties on deckWithCards, you have to check that it has been set to an actual value.
const [deckWithCards, setDeckWithCards] = useState(null);
const allCards = deckWithCards ? deckWithCards.cards : [];
Here we check if deckWithCards is truthy (not null). If we have a deck, then we access the cards from the deck. If it's still null, we use an empty array. Either way, allCards will always be an array that you can map, loop through, etc.
const fronts = allCards.map( card => card.front );
return (
<ul>
{allCards.map( (card) => (
<div className="card" key={card.id}>
{card.front}
</div>
))}
</ul>
)
First of all, the first state is []
const [deckWithCards, setDeckWithCards] = useState([]);
So in the very first run I except that deckWithCards is an array with no property cards. This can trigger the error you're encountering.
Otherwise, if deckWithCards is an array of objects, then to access the cards of, let's say, first object, you need to enter deckWithCards[0].cards
If instead you are correctly setting the value of deckWithCards with an object as you described above, deckWithCards.cards should correctly return the excepted list of cards.

Performing deep copy using spread operator - cannot copy last level of state

I have an app that has a list that when an item is clicked, populates another list with data that is in a sub-category of the selected list item. When a list item is clicked in the parent list, an API call goes and retrieves data to populate the child list. Multiple items result in multiple API calls.
When multiple items in the parent list are clicked, I need to add multiple children from each respective parent to the child list. I am trying to limit the number of dependencies in my app and therefore I am using as much ES6+ as possible.
I am using the spread operator to make multiple shallow copies of my nested state. The state slice I am struggling with looks like this:
{
tables: {
tablesLoading: false,
error: null,
selectedTables: [],
tables: {
byId: {
'9311fe20-dea9-11e9-ba2f-4fe2a4bdbda0': {
id: '9311fe20-dea9-11e9-ba2f-4fe2a4bdbda0',
value: 'Sample_1a',
fields: []
},
'9311fe21-dea9-11e9-ba2f-4fe2a4bdbda0': {
id: '9311fe21-dea9-11e9-ba2f-4fe2a4bdbda0',
value: 'Sample_1b',
fields: []
},
'93ea35b0-dea9-11e9-ba2f-4fe2a4bdbda0': {
id: '93ea35b0-dea9-11e9-ba2f-4fe2a4bdbda0',
value: 'Sample_2a',
fields: []
}
},
allIds: [
'9311fe20-dea9-11e9-ba2f-4fe2a4bdbda0',
'9311fe21-dea9-11e9-ba2f-4fe2a4bdbda0',
'93ea35b0-dea9-11e9-ba2f-4fe2a4bdbda0'
]
}
}
}
The initial API call pulls the data corresponding to the first two ID's ( '9311fe20-dea9-11e9-ba2f-4fe2a4bdbda0' and '9311fe21-dea9-11e9-ba2f-4fe2a4bdbda0') from a server. I then click another item in the parent list and want to add the third item to the tables.byId object and then add the corresponding ID to the tables.allIds array.
My code for that is:
const tableRequestSuccess = (state, action) => {
const tableVals = action.tableVals;
const datasetName = action.datasetName;
const tableUUIDs = tableVals[datasetName].map(val=>(uuidv1()));
let addedtablesById = {};
for (let i = 0; i<tableUUIDs.length; i++){
let tableId = tableUUIDs[i];
addedtablesById[tableId]={
id: tableId,
value: tableVals[datasetName][i],
fields: []
}
}
**const updatedTablesById = updateObject(state.tables.byId, addedtablesById)**
const updatetableAllIds = [...state.tables.allIds, ...tableUUIDs]
const updatedProperties = {
byId: updatedTablesById,
allIds: updatetableAllIds
};
return updateObject(state, {
tablesLoading: false,
error: null,
tables: updateObject(state.tables, updatedProperties)
});
}
My problem is that I am not sure how to do a copy on the part of my state tree where the individual objects are stored, i.e. state.tables.byId.THE_RESPECTIVE_IDS. This leaves me with the inner most objects having a reference to the original objects and therefore the state won't be updated immutably. But because the ID's will vary and the number will constantly change, I am not sure how to copy those last objects. I suspect all the state up to that point has been cloned over.
I have created a helper function for readability:
export const updateObject = (oldObject, updatedProperties) => {
return {
...oldObject,
...updatedProperties
}
}
Any pointers on how to get this done using the spread operator?

Categories