onClick loop happens only once per click - javascript

I have a label, which has onClick callback:
<label className="cursor-pointer" onClick={loadExisting}>
Click me
</label>
loadExisting function, fetch'es data from api, and passses it to parseData function.
const loadExisting = () => {
fetch("/api/v.1.0/events", { mode: "no-cors" })
.then(function(response) {
if (!response.ok) {
console.log("Something is wrong");
return;
}
return response.json();
})
.then(data => {
if (!data) {
return;
}
parseEvents(data);
});
};
In this function, I am trying to store only those events, which's titles are unique:
const parseEvents = data => {
if (data) {
for (let i = 0; i < data.length; i++) {
if (titlesArray.indexOf(data[i].title) < 0) {
setTitlesArray([...titlesArray, data[i].title]);
setEvents([...events, data[i]]);
}
}
}
};
Basically my idea is to set all the unique titles into titlesArray and if event's title is unique, I add it to both titlesArray and events.
Problem: This only works if I keep clicking on that label. With first click events.length is equal to 0, second click- equal to 1, third click- equal to 2, etc. Why it does not parse all events at once? So after 1 click I would have ~10 events that have unique titles.

setState is asynchronous and setTitlesArray and setEvents are likely to call setState.
So in your case, since you don't update titlesArray and events, the most likely is that you will only observe the last elem added
(data[data.length-1])
The fix is to simply call setState once.
data.forEach(d => {
if (titlesArray.includes(d.title) {
titlesArray.push(d.title)
events.push(d)
}
})
//I slice 0 to copy, but maybe it is not necessary if your function does it already
setTitlesArray(titlesArray.slice(0))
setEvents(events.slice(0))
nb: looking for existence in an array (titlesArray) is less efficient than using a Set. You should consider if you have a lot of titles

Related

Why does a list element not initially render but does after a state change in React?

If necessary, the project's current code be viewed on Github
I've ran into an issue with my current React/TypeScript project, a cocktail recipe viewer, where the ingredients list will not show up initially when a drink is selected; however, after a second drink is selected the ingredients list shows up as intended.
Screenshot of the ingredients not showing up during an initial search
Screenshot of the ingredients showing up correctly after a second drink is selected
Technical details:
The cocktail data is fetched from TheCocktailDB's free API and assigned to a "data" state. When a specific drink is selected it is set to a "drink" state. The drink's photo, ingredients, recipe, and directions of how to make the drink are shown in a modal pop-up.
I utilized useEffect to run two functions each time a drink is selected to parse the ingredients and measurements for the chosen drink:
useEffect(() => {
getIngredients(drink);
getMeasurements(drink); }, [drink]);
The getIngredients and getMeasurements functions are similar:
const getIngredients = (drink: IDrinkProps) => {
/*Each drink object has the properties strIngredient1 through strIngredient15
this checks each property to determine if it's a string and checks that it's not an empty string, if it passes
those two conditions then it is pushed to the filteredIngredients array and displayed in the modal popup for the drink the user selects.*/
for (let i = 0; i <= 14; i++) {
//Checks that the current ingredient is a string, if not the message is console logged
if (typeof drink[`strIngredient${i}`] !== "string") {
console.log(typeof drink[`strIngredient${i}`], "Detected");
//Checks if the current ingredient is an empty string if so the message is console logged
} else if (drink[`strIngredient${i}`] === "") {
console.log("Empty string detected");
} else {
//If the checks above pass the ingredient is pushed to the filteredIngredients array
filteredIngredients.push(drink[`strIngredient${i}`]);
}
//Once the for loop finishes the filteredIngredients array is set to the ingredients state and displayed in the modal pop-up
setIngredients(filteredIngredients);
}};
The getMeasurements function runs a third function, getExactRecipe upon it's conclusion:
const getMeasurements = (drink: IDrinkProps) => {
for (let i = 0; i <= 14; i++) {
//Checks that the current measurement is a string, if not the message is console logged
if (typeof drink[`strMeasure${i}`] !== "string") {
console.log(typeof drink[`strMeasure${i}`], "Detected");
//Checks if the current measurement is an empty string if so the message is console logged
} else if (drink[`strMeasure${i}`] === "") {
console.log("Empty string detected");
} else {
//If the checks above pass the measurement is pushed to the filteredMeasurements array
filteredMeasurements.push(drink[`strMeasure${i}`]);
}
//Once the for loop finishes the filteredMeasurements array is set to the measurements state and displayed in the modal pop-up
}
setMeasurements(filteredMeasurements);
getExactRecipe(ingredients, measurements); };
The getExactRecipe function combines the ingredients with their respective measurements for display in the modal pop-up:
const getExactRecipe = (
ingredients: (string | undefined)[],
measurements: (string | undefined)[]
) => {
let exactArray: string[] = [];
if (ingredients.length > 0) {
for (let i = 0; i < filteredIngredients.length; i++) {
if (
`${filteredIngredients[i]}` === "undefined" ||
`${measurements[i]}` === "undefined"
) {
//Do nothing
} else {
exactArray.push(`${filteredIngredients[i]} ${measurements[i]}`);
setExactRecipe(exactArray);
}
}
}
};
I am uncertain if this issue has to do with the useEffect statement or getExactRecipe function possibly. Or if it is caused by another unknown culprit. Any help is much appreciated as this has been driving me crazy!
I didn't check your entire code, as you should really try to condense your problem into a smaller example, but I'm guessing your issues is this...
useEffect(() => {
getIngredients(drink);
getMeasurements(drink);
}, [drink]);
...
const getIngredients = (drink: IDrinkProps) => {
...
setIngredients(filteredIngredients);
};
...
const getMeasurements = (drink: IDrinkProps) => {
...
getExactRecipe(ingredients, measurements);
};
You're using ingredients in the same useEffect block as setIngredients was called. I'm guessing you were hoping to use the newly set value, but that's not how hooks work. This means that your getMeasurements function is still using the previous ingredients value.
Note also that you're making this same mistake with setMeasurements and measurements.
An alternative to this, would be to split the useEffect in to 2 separate blocks.
useEffect(() => {
... // Ingredients
}, [drink]);
useEffect(() => {
... // Measurements
}, [drink, ingredients]); // Note: that this second useEffect also depends on ingredients!

Unable to get doc id in Firebase

I am trying to get the doc id of each data entry upon click to delete that specific record, but upon checking it is only showing id of the first entry made in the Firebase.
const deleteRecord = () => {
db.collection("records").onSnapshot((querySnapshot) => {
querySnapshot.forEach((doc) => {
// console.log(doc.id)
let recToDel = document.querySelectorAll(".records")
for (let toDelRecord of recToDel) {
toDelRecord.onclick = () => {
console.log(doc.id)
}
}
})
})
}
The loops are nested, so the last iteration of the querySnapshot.forEach loop is the one that sets the same doc.id for every recToDel dom element.
Fix by looping just one collection and indexing into the other...
let recToDel = Array.from(document.querySelectorAll(".records"));
querySnapshot.docs.forEach((doc, index) => {
if (index < recToDel.length) {
let toDelRecord = recToDel[index];
toDelRecord.onclick = () => {
console.log(doc.id)
}
}
});
If there are fewer .records elements than there are docs, some won't be assigned.
Doing this onSnapshot will cause these assignments to be made every time the collection changes, including on deletes. If that's not your intention, fix by changing to get().

Break For Each Loop in a Custom Cypress Command

I have a custom Cypress command to scroll through a list until you reach the item passed into the command. In my command I have $.each so I can compare the name of the item to the item name passed into the function. If they match then I send a click command which is "ENTER" in my environment.
I am able to successful scroll through the list and find the the item I am looking for and click on it but the loop continues to execute. I added return false which is what Cypress says should break the loop but it is now working for me. Any ideas why that is the case?
listItems.getChildren('OneLineTextElement', 'type').each(($elm) => {
let labelAndValue = cy.wrap($elm).getChildren('LabelAndValue')
let label = labelAndValue.getChild('Label')
label.getProperty('texture-text').then(val => {
if (val == subject) {
cy.action('ENTER')
return false
}
else {
cy.action('DOWN')
}
})
})
You can try adding a control variable in the the scope of the .each() command.
listItems.getChildren('OneLineTextElement', 'type').each(($elm) => {
let done = false;
let labelAndValue = cy.wrap($elm).getChildren('LabelAndValue')
let label = labelAndValue.getChild('Label')
label.getProperty('texture-text').then(val => {
if (val == subject) {
cy.action('ENTER').then(() => { // is cy.action a custom command?
// Likely you may need to wait
done = true;
})
} else {
cy.action('DOWN')
}
})
if (done) {
return false;
}
})
However there are some method calls that look like custom commands inside .each() so you may not get the flow of execution you expect (Cypress commands and test javascript can run asynchronously).
It looks like the code may be refactored to avoid "scrolling through the list". The only thing this does not do is cy.action('DOWN') on the non-subject list items.
listItems.getChildren('OneLineTextElement', 'type')
.getChildren('LabelAndValue')
.getChild('Label')
.getProperty('texture-text')
.should(val => {
expect(val).to.equal(subject);
cy.action('ENTER');
})
You have to use promise after finding your match within the loop, and then apply the assertion. Below an example searching value '23' within the column 8 which corresponds to age field.
cy.get('.column-8')
.each(ele => {
if (ele.text() === '23') {
isValuePresent = true
if (isValuePresent) return false
}
}).then(() => {
expect(isValuePresent).to.be.true
})

How to break the for loop using state

I have code as below.
I need to break the loop when first match is found.
const [isCodeValid, setIsCodeValid] = useState(false);
for (let i = 0; i < properyIds.length; i++) {
if (isCodeValid) {
break; // this breaks it but had to click twice so state would update
}
if (!isCodeValid) {
firestore().collection(`properties`)
.doc(`${properyIds[i]}`)
.collection('companies').get()
.then(companies => {
companies.forEach(company => {
if (_.trim(company.data().registrationCode) === _.trim(registrationCode.toUpperCase())) {
console.log("should break here")
// updating state like this wont take effect right away
// it shows true on second time click. so user need to click twice right now.
setIsCodeValid(true);
}
});
})
}
}
state won't update right away so if (!isCodeValid) only works on second click.
Once I find match I need to update state or variable so I can break the for loop.
I tried to use a variable but its value also not changing in final if condition, I wonder what is the reason? can anyone please explain ?
You should try and rewrite your code such that you will always call setIsCodeValid(value) once. In your case it could be called multiple times and it might not get called at all
const [isCodeValid, setIsCodeValid] = useState(false);
function checkForValidCode() {
// map to an array of promises for companies[]
const companiesPromises = properyIds.map(propertyId =>
firestore()
.collection(`properties`)
.doc(propertyId)
.collection('companies').get())
Promise.all(companiesPromises)
// flatten the 2d array to single array, re-create to JS array because of firestores internal types?
.then(companiesArray => [...companiesArray].flatMap(v => v))
// go through all companies to find a match
.then(companies =>
companies.find(
company => _.trim(company.data().registrationCode) === _.trim(registrationCode.toUpperCase())
))
.then(foundCompany => {
// code is valid if we found a matching company
setIsCodeValue(foundCompany !== undefined)
})
}
Try something like this:
import { useState } from 'react';
function YourComponent({ properyIds }) {
const [isCodeValid, setIsCodeValid] = useState(false);
async function handleSignupClick() {
if (isCodeValid) {
return;
}
for (let i = 0; i < properyIds.length; i++) {
const companies = await firestore()
.collection(`properties`)
.doc(`${properyIds[i]}`)
.collection('companies')
.get();
for (const company of companies.docs) {
if (_.trim(company.data().registrationCode) === _.trim(registrationCode.toUpperCase())) {
setIsCodeValid(true);
return;
}
}
}
}
return (<button onClick={handleSignupClick}>Sign Up</button>);
}
If you await these checks, that will allow you to sequentially loop and break out with a simple return, something you can't do inside of a callback. Note that if this is doing database queries, you should probably show waiting feedback while this is taking place so the user knows that clicking did something.
Update:
You may want to do all these checks in parallel if feasible so the user doesn't have to wait. Depends on your situation. Here's how you'd do that.
async function handleSignupClick() {
if (isCodeValid) {
return;
}
const allCompanies = await Promise.all(
properyIds.map(id => firestore()
.collection(`properties`)
.doc(`${properyIds[i]}`)
.collection('companies')
.get()
)
);
setIsCodeValid(
allCompanies.some(companiesSnapshot =>
companiesSnapshot.docs.some(company =>
_.trim(company.data().registrationCode) === _.trim(registrationCode.toUpperCase())
)
)
);
}
Can you not break it after setIsCodeValid(true);?
Use some:
companies.some(company => {
return _.trim(company.data().registrationCode) === _.trim(registrationCode.toUpperCase());
});
If some and forEach are not available then companies is not an array but an array-like object. To iterate through those, we can use for of loop:
for (const company of companies){
if (_.trim(company.data().registrationCode) === _.trim(registrationCode.toUpperCase())) {
// do something
break;
}
}
I tired below and it worked for me to break the loop.
I declared and tried to change this variable let codeValid and it was just not updating its value when match found. (not sure why)
But all of a sudden I tried and it just works.
I didnt change any actual code except for variable.
let codeValid = false;
let userInformation = []
for (let i = 0; i < properties.length; i++) {
console.log("called")
const companies = await firestore().collection(`properties`)
.doc(`${properties[i].id}`)
.collection('companies').get()
.then(companies => {
companies.forEach(company => {
if (_.trim(company.data().registrationCode) === _.trim(registrationCode.toUpperCase())) {
// a += 1;
codeValid = true;
userInformation.registrationCode = registrationCode.toUpperCase();
userInformation.companyName = company.data().companyName;
userInformation.propertyName = properties[i].propertyName;
}
});
})
if (codeValid) {
break;
}
}

Using onClick to remove item from one array and place in another in React

I want to be able to click on a certain element and then remove it from the player1 array and place it into the playCard array. Currently this is attached to a button on the page.
choseCard = () => {
this.setState(({
playCard,
player1
}) => {
return {
playCard: [...playCard, ...player1.slice(0, 1)],
player1: [...player1.slice(1, player1.length)]
};
});
}
Currently this takes the first item from the player1 array and places it in the playCard array. I want to be able to select a certain element(card) from the player array instead of just taking the first element. I'm having a hard time thinking of the way to do this in react as I am still a beginner.
Is there a way to maybe move the selected card to the first element then use the above code?
try passing the index of element you are clicking on to the function, this might possibly work...
choseCard = (index) => {
this.setState(({playCard, player1}) =>
{
return
{
playCard: [...playCard, ...player1.slice(index, 1)],
player1: [...player1.slice(0, index),...player1.slice(index,player1.length)]
};
});
}
You can insert whatever logic you need right above the return statement, then return the results:
choseCard = () => {
this.setState(({playCard, player1}) => {
// change `index` to whichever index you want to remove
const index = 1;
const toInsert = player1[index];
const player1Copy = player1.slice();
player1Copy.splice(index, 1);
const playCardCopy = playCard.slice();
playCardCopy.push(toInsert);
return {
playCard: playCardCopy,
player1: player1Copy,
};
});
}
you could pass the card object in the choseCard function
choseCard(card) => {
const {playCard, player1} = this.state;
return this.setState({
playCard: playCard.concat(card),
player1: player1.filter(c => c.id !== card.id)
});
}
this is also under the assumption that each card has a unique identifier.

Categories

Resources