I ran into a roadblock when trying to update a hook when the web socket is called with new information and noticed that the hooks are returning the default values I set them to inside my useEffect, whilst inside the render it is returning the correct values. I am completely stumped and unsure why and was curious as to if anyone could help, much appreciated.
const [view, setView] = useState(false)
const [curFlip, setFlip] = useState(null)
tradeSocket.addEventListener('message', async (msg) => {
const message = JSON.parse(msg.data)
if (message.tradelink) {
// not needed
} else if (message.redItems || message.blueItems) {
// not needed
} else if (message.flips) {
console.log('effect ', view, curFlip) // this is where the issue occurs, it returns false and null
if (view && curFlip) {
console.log('theyre viewing a flip')
for (let i = 0; i < message.flips.length; i++) {
console.log('looping ' + i, message.flips[i].offer)
if (message.flips[i].offer === curFlip.offer) {
setFlip(message.flips[i])
}
}
}
setCoinflips(message.flips)
} else if (message.tradeid) {
// not needed
}
})
Image of what values it returns per render / effect called.
Based on our output, it seems that you set up the socket listener only once on initial render in useEffect.
Now since the useEffect callback is run once, the values used from closure inside the listener function will always show the initial valued
The solution here is to add view and curFlip to dependency array of useEffect and close the socket in useEffect cleanup function
useEffect(() => {
tradeSocket.addEventListener('message', async (msg) => {
const message = JSON.parse(msg.data)
if (message.tradelink) {
// not needed
} else if (message.redItems || message.blueItems) {
// not needed
} else if (message.flips) {
console.log('effect ', view, curFlip) // this is where the issue occurs, it returns false and null
if (view && curFlip) {
console.log('theyre viewing a flip')
for (let i = 0; i < message.flips.length; i++) {
console.log('looping ' + i, message.flips[i].offer)
if (message.flips[i].offer === curFlip.offer) {
setFlip(message.flips[i])
}
}
}
setCoinflips(message.flips)
} else if (message.tradeid) {
// not needed
}
})
return () => {
tradeSocket.close();
}
}
}, [curFlip, view]);
Related
I want to check on load if the object window has the property VL.refCode.
I use viral loops for my website and viral loop put in window.VL object the property refCode when a user is registered.
I put a for loop in a useEffect to check if this property exists or not, but unfortunately it becomes an infinite loop.
useEffect(() => {
if(window.VL){
if(window.VL.refCode){
setCryptoButtonMessage(t('common:cryptoButton_already_register'))
console.log('useEffect :user already register')
}
else{
console.log('useEffect : window.vl.refcode not exist')
}
}
else {
setCryptoButtonMessage(t('common:cryptoButton_title_waitlist'))
}
// t is for translation with i18n
},[t]);
This solution doesn't work because viral loop create window.VL object 1 to 2sec max after the first render.
If I put a setTimeout it's not a valid solution for users with mobile device / slow 3g / without fiber
So i use this solution
useEffect(() => {
let animation;
const check = () => {
console.log('je suis dans check');
console.log('je suis dans for');
if ((Object.prototype.hasOwnProperty.call(window.VL, 'refCode'))) {
console.log('je trouve refCode ');
setCryptoButtonMessage(t('common:cryptoButton_already_register'))
cancelAnimationFrame(animation);
return;
}
animation = requestAnimationFrame(check);
};
animation = requestAnimationFrame(check);
},[t]);
But this solution will never stop until window.VL.refCode exist ... not really best solution for website performance ...
I try to put a " simulate timer " but it becomes an infinite loop ...
useEffect(() => {
for (let i = 1; i < 10000; i += 1) {
let animation;
const check = () => {
if ((Object.prototype.hasOwnProperty.call(window.VL, 'refCode'))) {
setCryptoButtonMessage(t('common:cryptoButton_already_register'))
cancelAnimationFrame(animation);
return;
}
animation = requestAnimationFrame(check);
};
animation = requestAnimationFrame(check);
}
},[t]);
I don't know of any way of being automatically told when an object gets created on the window - so as far as I know the only way to do this is with a poll. If it was me, I'd abstract the logic for polling for the ref code into a new hook:
import { useEffect, useRef, useState } from 'react';
const useRefCode = (pollTimeMillis = 1000) => {
const timeoutRef = useRef(undefined);
const [refCode, setRefCode] = useState(window.VL?.refCode);
const cancelTimeout = () => {
if (!timeoutRef.current) return;
clearTimeout(timeoutRef.current);
timeoutRef.current = undefined;
};
useEffect(() => {
if (refCode) return;
setTimeout(() =>
setRefCode(window.VL?.refCode || undefined),
pollTimeMillis
);
return cancelTimeout;
}, [refCode]);
return refCode;
}
Then your other component that wants to change the message based on the ref code becomes very simple:
const OtherComponent = () => {
const refCode = useRefCode();
const translationKey = refCode
? 'common:cryptoButton_already_register'
: 'common:cryptoButton_title_waitlist';
return (
<div>{t(translationKey)}</div>
);
}
I'm trying to read the number of times an event has been logged and increment/decrement a variable accordingly. Since I can't use this.setState inside the getPastEvent (because it generates an Unhandled Runtime Error which is TypeError: Cannot read property 'setState' of undefined), I'm opting for this method where I perform my counting on a local variable then save it to the state variable.
The issue here is when I use this.setState({totalBidders: biddersnumber}); at the end of the function, I receive the value zero where in my case it should be two! How can I get the value of the counter biddersnumber in this situation?
componentDidMount = async () => {
const accounts = await web3.eth.getAccounts();
const plasticBaleSC = plasticBaleContract(this.props.address);
var biddersnumber = 0;
var highestbid = 0;
plasticBaleSC.getPastEvents('allEvents', { fromBlock: 0, toBlock: 'latest' }, function (error, events) {
console.log(events);
events.forEach(myfunction);
function myfunction(item, index) {
if (item.event === 'bidderRegistered') {
console.log(item);
biddersnumber++;
//value is two here
console.log(biddersnumber);
} else if (item.event === 'bidPlaced') {
} else if (item.event === 'bidderRegistered') {
} else if (item.event === 'bidderExited') {
console.log(item);
biddersnumber--;
} else if (item.event === 'auctionStarted') {
}
}
});
//Value is zero here
this.setState({ totalBidders: biddersnumber });
}
Using comments on this question, I did the following updates to resolve the issue. Thank you Andrea!
componentDidMount = async () => {
const accounts = await web3.eth.getAccounts();
const plasticBaleSC = plasticBaleContract(this.props.address);
var biddersnumber = 0;
var highestbid =0;
plasticBaleSC.getPastEvents("allEvents",{fromBlock: 0, toBlock:'latest'},(error, events)=>{
console.log(events);
const myfunction = (item,index) => {
if(item.event==='bidderRegistered'){
console.log(item);
biddersnumber++;
console.log(biddersnumber);
}else if(item.event==='bidPlaced'){
}else if(item.event==='bidderRegistered'){
}else if (item.event==='bidderExited'){
console.log(item);
biddersnumber--;
}else if(item.event==='auctionStarted'){
//console.log(item);
//this.setState({highestBid: item.returnValues['startingAmount']});
}
};
events.forEach(myfunction);
this.setState({totalBidders: biddersnumber});
});
};
You need to put the call to setState inside your callback:
// ....
var highestbid =0;
const that = this
// ....
// Value is two here
that.setState({totalBidders: biddersnumber});
});
//Value is zero here
};
You'll need to deal with this, so assign it to that like it's 2010.
I'm having this table view in React where I fetch an array from API and display it. When the user types something on the table search box, I'm trying to clear the current state array and render the completely new result. But for some reason, the result keeps getting appended to the current set of results.
Here's my code:
const Layout = () => {
var infiniteScrollTimeout = true;
const [apiList, setapiList] = useState([]);
//invoked from child.
const search = (searchParameter) => {
//Clearing the apiList to load new one but the console log after setApiList still logs the old list
setapiList([]); // tried setApiList([...[]]), copying the apiList to another var and emptying it and then setting it too.
console.log(apiList); //logs the old one.
loadApiResults(searchParameter);
};
let url =
AppConfig.api_url + (searchParameter || "");
const loadApiResults = async (searchParameter) => {
let response = await fetch(url + formurlencoded(requestObject), {
method: "get",
headers: headers,
});
let ApiResult = await response.json(); // read response body and parse as JSON
if (ApiResult.status == true) {
//Set the url for next fetch when user scrolls to bottom.
url = ApiResult.next_page_url + (searchParameter || "");
let data;
data = ApiResult.data;
setapiList([...data]);
}
}
useEffect(() => {
loadApiResults();
document.getElementById("myscroll").addEventListener("scroll", () => {
if (
document.getElementById("myscroll").scrollTop +
document.getElementById("myscroll").clientHeight >=
document.getElementById("myscroll").scrollHeight - 10
) {
if (infiniteScrollTimeout == true) {
console.log("END OF PAGE");
loadApiResults();
infiniteScrollTimeout = false;
setTimeout(() => {
infiniteScrollTimeout = true;
}, 1000);
}
}
});
}, []);
return (
<ContentContainer>
<Table
...
/>
</ContentContainer>
);
};
export default Layout;
What am I doing wrong?
UPDATE: I do see a brief moment of the state being reset, on calling the loadApiResult again after resetting the state. The old state comes back. If I remove the call to loadApiResult, the table render stays empty.
add apiList in array as the second parameter in useEffect
You need to use the dependencies feature in your useEffect function
const [searchParameter, setSearchParameter] = useState("");
... mode code ...
useEffect(() => {
loadApiResults();
... more code ...
}, [searchParameter]);
useEffect will automatically trigger whenever the value of searchParameter changes, assuming your input uses setSearchParameter on change
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;
}
}
Purpose of my code, is to fire fetchNumbers() (that fetches numbers from API) when a user scrolls bottom of the page, some kind of infinite scroll. I'm having issue with condition inside axios promise (then), because it fires two console.log outputs at the same time. Seems like condition is ignored at all.
Method i'm using:
methods: {
fetchNumbers (type = 'default', offset = 0, limit = 0) {
return axios.get(globalConfig.NUMBERS_URL)
.then((resp) => {
if (type === 'infinite') {
console.log('infinite fired')
} else {
console.log('default fired')
}
})
}
Mounted function (where i suspect the issue):
mounted () {
window.onscroll = () => {
let bottomOfWindow = document.documentElement.scrollTop + window.innerHeight > document.documentElement.offsetHeight - 1
if (bottomOfWindow) {
this.fetchNumbers('infinite')
}
}
}
When i reload the page, or enter it, i'm getting 2 console outputs at the same time:
default fired
infinite fired
Sometimes the order is reversed.
UPDATE.
Methods that calling fetchNumbers()
async created () {
await this.fetchNumbers()
}
showCats (bool) {
this.showCategories = bool
if (!bool) {
this.category = [1]
} else {
this.category = []
}
this.isActive = true
this.fetchNumbers()
}
UPDATE 2.
Found the culprit - would post it in an answer.
UPDATE 3.
Issue is indeed with onscroll function. I have 3 pages in my APP: main page, numbers page, contact page. If i go to the numbers page (where onscroll function is mounted), then go to main or contact page, then this onscroll function is still attached and when i reach the bottom - it fires my api call, even if it's not the numbers page. Is it possible to limit this function only to numbers page?
I had to disable onscroll listener on destroy:
destroyed () {
window.onscroll = null
}
So when i visit the main or contact page, that listener won't be attached.
Also i had to move onscroll listener from mounted to created, because when it was in mounted () it was firing twice:
created () {
window.onscroll = () => {
let bottomOfWindow = document.documentElement.scrollTop + window.innerHeight > document.documentElement.offsetHeight - 1
if (bottomOfWindow) {
this.isActive = true
this.fetchNumbers('infinite', counter++ * this.limit)
}
}
}
fetchNumbers is most definetly called twice.
Try log every context as type.
fetchNumbers (type = 'default', offset = 0, limit = 0) {
return axios.get(globalConfig.NUMBERS_URL)
.then((resp) => {
if (type === 'infinite') {
console.log(type,'infinite fired'); //this will always print 'infinite', 'inifinite fired'
} else {
console.log(type,'default fired'); //this will print the caller's name and 'default fired'
type = 'default'; //after logging reset type to 'default' to continue the execution
}
})
}
async created () {
await this.fetchNumbers('created');
}
showCats (bool) {
this.showCategories = bool
if (!bool) {
this.category = [1];
} else {
this.category = [];
}
this.isActive = true
this.fetchNumbers('showCats');
}
Either created or showCats is being fired for some reason at the same time as window.onscroll. The randomly changing order suggests a race condition between the two methods.
UPDATE
This should work, provided you nested the desired page in a div with id="number_page_div_id" (you may probably have to adjust that elements height and position):
created () {
let element = getElementById('numbers_page_div_id');
element.onscroll = () => {
let bottomOfWindow = element.scrollTop + window.innerHeight > element.offsetHeight - 1;
if (bottomOfWindow) {
this.fetchNumbers('infinite')
}
}
}
you may approach separate condition instead of if/else e.g.
.then((resp) => {
if (type === 'infinite') {
console.log('infinite fired')
}
if (type === 'default '){
console.log('default fired')
}
})