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

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.

Related

Null values after mapping Data to an Object

I am trying to map multiple Database entries to an Object, so I can filter it.
async autocomplete(interaction) {
const focusedValue = interaction.options.getFocused();
let choices = await items.findAll({
attributes: ['itemID', 'desc_de'] });
const jsondata = JSON.stringify(choices);
const json = JSON.parse(jsondata);
const itemObj = json.map(function(item) {
return {
itemID: item.itemID,
item_desc: item.desc_de,
};
});
const filtered = itemObj.filter(choice => choice.item_desc.toLowerCase().includes(focusedValue.toLowerCase()));
await interaction.respond(
filtered.map(choice => ({name: choice.item_desc, value: choice.itemID})),
);
The issue I am having is, apparently choice.item_desc is NULL somehow, I am unsure, what I am doing wrong here.
The error is
TypeError: Cannot read properties of null (reading 'toLowerCase')
Before I had an object, which was only holding the itemID and with that it was working fine.
focusedValue may be null to which you cannot apply .toLowerCase(). Try setting a default value to that given that function exists outside the scope of the code provided

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 am I unable to map this array of objects when it is loaded in useState()?

learning React but I'm trying to map an array of objects in a dropdown box.
I'm getting a "Uncaught TypeError: ids.map is not a function".
Why am I getting this error when I have set the loadedIds I get from the GET request into useState's setIds?
https://jsfiddle.net/4jh9c6dv/53/
Thank you for helping a beginner :')
function Dropdown ()
{
const [ids, setIds] = React.useState([]);
React.useEffect(() => {
request.get(endpointIds).then((response) => {
setIds(response.data);
const loadedIds = [];
for (const id in response)
{
loadedIds.push({
id: ids,
});
}
setIds(loadedIds);
});
}, []);
const idsList = ids.map((id) =>
(
<option>id</option>
));
You do not need to use setIds(response.data);. Remove that.
response.data is not an array, response.data.ids is. That is why you get the error.
EDIT : Use for of instead of for in.
for of is used to iterate over items of an array/iterable object.
for in is used to iterate over index.
response.data should be response.data.id
you also forgot curly braces {id}

Appending multi dimensional array in react state

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.

Object assigning in JavaScript

There is an object product which has object manufacturer as its field. After fetching manufacturers from my server, I reassign product's field manufacturer with a new data (for example, fetched manufacturer has additional object avatar as its field).
async componentDidMount() {
strapi.setToken(this.state.user.jwt);
const products = await strapi.getEntries('products');
products.forEach(async product => {
const manufacturer = await strapi.getEntry('manufacturers', product.manufacturer.id); //fetched manufacturers has additional field "avatar"
Object.assign(product.manufacturer, manufacturer);
});
console.log(products); // product.manufacturer.avatar is not null
this.setState({
products
});
Then I'm trying to display avatar in React render().
render() {
if (!this.state.products) return null;
return(
{this.state.products ? this.state.products.map(product => (
// and it says that avatar is null
<img src={product.manufacturer.avatar.url} />
// displays manufacturer with avatar object
{console.log(product.manufacturer)}
// says null!
{console.log(product.manufacturer.avatar)}
))}
)
}
Also when I check state with React Developer Tools, product's field manufacturer has object avatar and it isn't null.
UPDATE:
Thanks to Sergey Suslov
strapi.setToken(user.jwt);
const fetchedProducts = await strapi.getEntries('products');
const promices = fetchedProducts.map(async fetchedProduct => {
const manufacturer = await strapi.getEntry('manufacturers', fetchedProduct.manufacturer.id);
const product = { ...fetchedProduct, manufacturer };
return product;
});
Promise.all(promices).then(products =>
this.setState({
products
})
);
You are assigning with the assign method that does not mutate source object, it returns new object as a result, check this https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign.
You need to do semthing like this:
product = {
...product,
manufacturer: your assign statment
}
Main reason is that your callback function in foreach is asynchronous, js does not wait for all foreach function calls to completed, it keep on running, couse async word, try to use Promise for this, or better try to use actions for async requests, thank librariy or sagas.
It's because at first render, you do not have the avatar value. These values are fetched after the first render (componentDidMount).
You need to add a test to take that first render into account.
Also, the reason why your console.log is not consistent, is because you are overwriting the value of product, to be more immutable, you should .map instead of forEach, and return a copy of your product instead of modifying the existing one.
How I would write it:
constructor(props){
super(props);
this.state = {
products: []
}
}
render() {
return this.state.products.map(product => {
const {
avatar = {}
} = product.manufacturer;
return (
<img src={avatar.url} />
);
});
}

Categories