I'm trying to get the coords of the user and once done I'm trying to use "useState" to make that position global but for some reason it's always returning undefined the first time it's ran even though I'm using a promise.
const [globalPosition, setGlobalPosition] = useState<any>(undefined);
useEffect(() => {
const getLocation = new Promise<void>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(success, error, {
enableHighAccuracy: true,
});
function success(position: any) {
setGlobalPosition(position);
resolve();
}
function error(err: any) {
console.log(err);
reject();
}
});
getLocation.then(() => {
console.log(globalPosition);
});
}, []);
The first render will always be undefined because that's what you defined as the initial value.
const [globalPosition, setGlobalPosition] = useState<any>(undefined);
// ^^^^^^^^^
When a state setting function is called, such as inside your useEffect, the component will rerender.
If you don't want to continue rendering while undefined, after that useEffect add one of these options (or your own loading state):
if (globalPosition == undefined) return null;
if (globalPosition == undefined) return <div>Loading...</div>;
Also because of how React state and const work, getLocation.then's globalPosition will not be the new version of the variable after you change it since it's storing a copy/reference to the original state at the beginning of that render.
Related
I am trying to build a cryptocurrency application. But I am having trouble getting populated state values that are passed into the useEffect as parameters to a debouncing/polling function.
The issue is that the debouncing works well, as in it detects the value and calls the api after the 500ms that I specified in debounce. However, the polling portion seems to not have the state values of of transactionType, fromCurrencyAmount, and pair. It seems like after I debounce the input, after 6 seconds the polling will do its thing but the values passed in the params are undefined. Is there anyway I can solve this?
Here is the method that serves two purposes. It has an api to be polled from every 6 seconds, as well as getting debounced input if the user enters an amount inside the input.
function handleInitPoll(baseAndQuote, side, value) {
getSwapPrice(baseAndQuote, side, value || 0)
.then((res) => {
if (!res.price) {
setIsLoading(true);
} else if (res.error) {
setErrorMessage(res.error);
} else if (res.price) {
setIsLoading(false);
setSwapPriceInfo(res);
}
});
}
And here is the useEffect:
useEffect(() => {
handleInitPoll(pair, transactionType, fromCurrencyAmount);
const timer = setInterval(handleInitPoll, 6000, pair, transactionType, fromCurrencyAmount);
return () => {
clearInterval(timer);
};
}
setSelectedCurrencyState({ ...selectedCurrencyState, selectedFromCurrency: 'USDT', selectedToCurrency: 'XLM' });
}, [pair, transactionType, fromCurrencyAmount]);
And here is the debounce declaration:
const debounceOnChange = useCallback(debounce(handleInitPoll, 500, pair, transactionType, fromCurrencyAmount), []);
And here is where the debouncing is being done, which is inside an onChange handler:
const handleAssetAmount = (e) => {
const { value } = e.target;
const formattedAmount = handleAssetAmountFormat(value);
setFromCurrencyAmount(formattedAmount);
validateInputAmount(formattedAmount);
debounceOnChange(pair, transactionType, formattedAmount);
};
Issue
The issue here is that you've closed over stale values in the interval callback.
Solution
One solution is to cache these state values in a React ref such that the current value can be accessed in the polling function.
Example:
const pairRef = React.useRef(pair);
const transactionTypeRef = React.useRef(transactionType);
const fromCurrencyAmountRef = React.useRef(fromCurrencyAmount);
useEffect(() => {
pairRef.current = pair;
}, [pair]);
useEffect(() => {
transactionTypeRef.current = transactionTypeRef;
}, [transactionType]);
useEffect(() => {
fromCurrencyAmountRef.current = fromCurrencyAmount;
}, [fromCurrencyAmount]);
useEffect(() => {
handleInitPoll(pair, transactionType, fromCurrencyAmount);
const timer = setInterval(() => {
handleInitPoll(
pairRef.current,
transactionTypeRef.current,
fromCurrencyAmountRef.current
);
}, 6000);
return () => {
clearInterval(timer);
};
}, [pair, transactionType, fromCurrencyAmount]);
Fundamentally, your code seems to be correct with a few issues:
There is a race condition.
If getSwapPrice is running and the component is updated, it can still
affect the state when setSwapPriceInfo or setLoading are called when
the promise is resolved.
This is particularly bad, because network requests can "overtake" each
other. Thus it can happen that the return value of getSwapPrice
updates the component with the result of an old network request.
This is discussed in this article.
There is this odd call to setSelectedCurrencyState in the useEffect
block. It's not clear what this is supposed to do, but it clearly doesn't
belong there.
However, the underlying application should work fine, I reproduced it with a simpler application here:
import { useEffect, useState } from "react";
function fetchExchangeRateAsync(multiplier) {
return new Promise(resolve => {
setTimeout(() => {
resolve(Math.random() * multiplier);
}, 100);
});
}
function CurrencyExchangeRate() {
const [exchangeRate, setExchangeRate] = useState(null);
const [inputValueString, setInputValueString] = useState("");
const inputValue = Number(inputValueString);
const [multiplier, setMultiplier] = useState(1.0);
let outputValue = null;
if (!isNaN(inputValue) && exchangeRate !== null) {
outputValue = inputValue * exchangeRate;
}
useEffect(() => {
// To avoid race conditions, we must not update the state from an asynchronous operation if
// the component was re-rendered since then.
//
// https://overreacted.io/a-complete-guide-to-useeffect/
let didCancel = false;
// Do not delay the first request.
fetchExchangeRateAsync(multiplier)
.then(newExchangeRate => {
if (!didCancel) {
setExchangeRate(newExchangeRate);
}
});
// Poll exchange rate.
let intervalHandle = setInterval(() => {
fetchExchangeRateAsync(multiplier)
.then(newExchangeRate => {
if (!didCancel) {
setExchangeRate(newExchangeRate);
}
});
}, 500);
return () => {
didCancel = true;
clearInterval(intervalHandle);
};
}, [multiplier]);
return (
<div>
<input value={inputValueString} onChange={event => setInputValueString(event.target.value)} /><br />
<p>With current exchange rate: {outputValue !== null ? outputValue : "(loading)"}</p>
<button onClick={() => setMultiplier(100.0)}>Set Multiplier</button>
</div>
);
}
function App() {
return (
<CurrencyExchangeRate />
);
}
export default App;
This is quite a bit different from what you are doing but it does demonstrate that your code should generally work:
It is possible to trigger the fetch logic by changing the input field (here without debouncing) or in a given interval.
The fetch logic runs immediately when the component is rendered for the first time.
The "Set Multiplier" button can affect the value of multiplier and this information arrives in the setInterval call correctly.
This works because [multiplier] dictates that the effect should be re-run if that variable changes. When this happens, the old interval is first cleared with clearInterval and then re-started with setInterval.
In your case that would be pair, transactionType and fromCurrencyAmount instead of multiplier.
In other words, your issue seems to be outside the code that you provided in the question.
I use hook useState for set post value.
const [firstPost, setFirstPost] = useState();
useEffect(() => {
(async () => { await onFetchPosts(); })();
}, []);
const onFetchPosts = async () => {
try {
const { body } = await publicService.fetchPostById(119);
// get post
const post = body.posts;
if (post && post.postsId) {
console.log(`save...`, body.posts);
setFirstPost(body.posts);
}
console.log(`firstPost...`, firstPost);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
}
I dont understand, firstPost is not updated.
This is because setState calls are asynchronous. Read it here and here. As the per the doc I linked
setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall
Therefore, the state is usually not updated yet when you console.log it on the next line, but you can access/see the updated state on the next render. If you want to log values, you can put them as inside a <pre> tag in your HTML, or do console.log at the beginning, like below:
const [firstPost, setFirstPost] = useState();
// Console.log right on at the start of the render cycle
console.log("First post", firstPost);
useEffect(() => {
(async () => {
await onFetchPosts();
})();
}, []);
const onFetchPosts = async () => {
try {
const { body } = await publicService.fetchPostById(119);
// get post
const post = body.posts;
if (post && post.postsId) {
console.log(`save...`, body.posts);
setFirstPost(body.posts);
}
// Do not console.log the state here
// console.log(`firstPost...`, firstPost);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
// Can also debug like this
return <pre>{JSON.stringify(firstPost)}</pre>;
React may batch multiple setState() calls into a single update for performance.
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
React does not change the state variables immediately when state is changed i.e why you are getting undefined in console.
However the new value of firstPost is assured to be there in the next render.
I want to stop making API calls when a variable become undefined.
ngOnInit() {
// Set time interval to update data again and again
const source = interval(3000);
if(this.groupId){
this.subscription = source.subscribe(
val => this.mrDataService.getGaugeData(this.groupId).then(
data => {
if(this.groupId){
console.log(data)
}
},
error => {
console.log('Something Went Wrong')
}
)
);
}
}
ngOnDestroy(){
this.groupId=undefined;
}
I need to set this.groupId to undefined when component is destroyed. When I console.log(this.groupId) in the ngOnDestroy it prints undefined but the interval keep making calls.
The services file:
getGaugeData(groupId:any){
return this.http.get(this.path, {headers: {'tokenid': this.tokenId},params: {groupId: '3133'})
.toPromise().then(res => {
return res;
});
}
I don't misunderstood, you need to unsuscribe the Observable, then, only must call this function.You could use doCheck, called ensure before groupId become undefined, and unsuscribe the susbscription.
doCheck() {
if(!this.groupId) {
unsuscribe().
}
}
I'm trying to set the state of a property after making an API call only if the component hasn't been unmounted. In the first function the variable "unmounted" is initialize inside the function "Component"; in this case I'm getting this warning: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application."
In the second function I initialize the variable "unmounted" globally and in this case I'm not getting any warning.
Shows a warning:
function Component() {
const [emailSent, setEmailSent] = useState(false);
var unmounted = false;
async function handleClickEvent() {
try {
await AuthApi.sendRecoverAccountEmail('123');
!unmounted && setEmailSent(true);
} catch (err) {
!unmounted && setIsSendingEmail(false);
}
}
useEffect(() => {
return () => {
unmounted = true;
};
}, []);
}
No warnings:
var unmounted = false;
function Component() {
const [emailSent, setEmailSent] = useState(false);
async function handleSendEmail(formValues) {
try {
await AuthApi.sendRecoverAccountEmail('123');
!unmounted && setEmailSent(true);
} catch (err) {
!unmounted && setIsSendingEmail(false);
}
}
useEffect(() => {
return () => {
unmounted = true;
};
}, []);
}
Anyone can explain why is this happening?
On your first example, unmounted will always be false after each render.
Here's the right way without using an global instance:
function Component() {
const [emailSent, setEmailSent] = useState(false);
const unmounted = useRef(false);
async function handleSendEmail(formValues) {
try {
await AuthApi.sendRecoverAccountEmail('123');
!unmounted.current && setEmailSent(true);
} catch (err) {
!unmounted.current && setIsSendingEmail(false);
}
}
useEffect(() => {
return () => {
unmounted.current = true;
};
}, []);
}
An interesting question. You might find the FAQ about hooks useful, as I think it addresses your question rather specifically.
In your first example, your unmounted var is part of your component properties, where in the second example, it is just picked up as part of the javascript closure.
Usually, you want to make sure that you change the mounted parameter as part of the componentDidUnmount lifecycle method.
I think if you add unmounted into the list of dependencies, it might work? I normally use react from another framework in Clojurescript, so I'm not completely familiar with the semantics of the main javascript interface, but that would at least be why you're getting a warning for the first example.
In your first example, what happens if you change the last part to the following?
useEffect(() => {
return () => {
unmounted = true;
};
}, [unmounted]);
}
You may need to define unmounted as a more formal react property instead of just a property enclosed in your component.
I have a React app which uses Redux and axios. I do not want anything to render before I have some information from the server, which I am retrieving via axios.
I thought that the best way to do this would be by initializing redux state based on that axios call.
However, my function does not seem to be returning anything in time for state initialization...
function getUserData() {
if (Auth.loggedIn()) { //leaving this here bc it could be important; Auth is another class and loggedIn returns a boolean
axios.get('/route').then(res => {
console.log(res.data); //This prints the right thing (an object)
return res.data;
});
} else {
return ' '; //This works fine; state gets initialized to ' '
}
}
let userData = getUserData();
console.log(userData); //When getUserData() returns ' ', this prints ' '. However, when getUserData() returns my object, this prints undefined.
const initialState = {
userData: userData
};
I realize that this could be a problem with getUserData() being asynchronous, and console.log(userData) running before getUserData() has finished. However, I tried:
getUserData().then(function(userData) {
console.log(userData);
});
And received 'TypeError: Cannot read property 'then' of undefined'. My function is obviously not returning a promise, so doesn't that mean it's not asynchronous?
Any ideas?
Alternatively, is there a better way of doing this? I could always set initial state and then immediately change it, and make rendering wait for the change to be complete with a conditional render, but that definitely seems worse.
You have to return promise from your getUserData function and access it using .then().
function getUserData() {
return new Promise((resolve, reject) => {
if (Auth.loggedIn()) {
axios.get('/route')
.then(res => resolve(res.data))
.catch(err => reject(err));
} else {
resolve(' ');
}
});
};
getUserData().then(function(userData) {
const initialState = {
userData: userData, // ' ' or axios result
};
console.log(initialState.userData)
});