useContext not updating its value in callback - javascript

useConext value is not updating in my callback attached to wheel event. I tried to console but still printing static value. But outside the callback, it's printing updated value
const Home = () => {
//accessing my context
var [appState, dispatch] = useContext(CTX);
//printing updated value here (working perfect here)
console.log(appState);
//my callback on wheel event (also using debouce to queue burst of events)
var fn = debounce(e => {
//incrementing value ++1
dispatch({ type: 'INCREMENT_COMPONENT_COUNTER' });
//printing static value here (problem here)
console.log(appState);
}, 500);
//setting and removing listener on component mount and unmount
useEffect(() => {
window.addEventListener('wheel', fn);
return () => {
window.removeEventListener('wheel', fn);
};
}, []);
};

On mounting, the listener initialized with a function variable which encloses the first value of appStore in its lexical scope.
Refer to Closures.
To fix it, move it into useEffect scope.
const Home = () => {
const [appState, dispatch] = useContext(CTX);
useEffect(() => {
const fn = debounce(e => {
dispatch({ type: 'INCREMENT_COMPONENT_COUNTER' });
console.log(appState);
}, 500);
window.addEventListener('wheel', fn);
return () => {
window.removeEventListener('wheel', fn);
};
}, [appState]);
};
Friendly advice:
Use linter like eslint - It should have warned you of using appState inside useEffect
Don't use var - it's error-prone.

Your debance function is changing in every render, while the useEffect have the capture of only the first render, you can fix this with a useCallback:
const Home = () => {
// accessing my context
const [appState, dispatch] = useContext(CTX)
// printing updated value here (working perfect here)
console.log(appState)
// my callback on wheel event (also using debouce to queue burst of events)
const fn = useCallback(
() =>
debounce(e => {
// incrementing value ++1
dispatch({ type: 'INCREMENT_COMPONENT_COUNTER' })
// printing static value here (problem here)
console.log(appState)
}, 500),
[appState, dispatch],
)
// setting and removing listener on component mount and unmount
useEffect(() => {
window.addEventListener('wheel', fn)
return () => {
window.removeEventListener('wheel', fn)
}
}, [fn])
}

Related

custom hook memory leak [duplicate]

When fetching data I'm getting: Can't perform a React state update on an unmounted component. The app still works, but react is suggesting I might be causing a memory leak.
This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function."
Why do I keep getting this warning?
I tried researching these solutions:
https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
https://developer.mozilla.org/en-US/docs/Web/API/AbortController
but this still was giving me the warning.
const ArtistProfile = props => {
const [artistData, setArtistData] = useState(null)
const token = props.spotifyAPI.user_token
const fetchData = () => {
const id = window.location.pathname.split("/").pop()
console.log(id)
props.spotifyAPI.getArtistProfile(id, ["album"], "US", 10)
.then(data => {setArtistData(data)})
}
useEffect(() => {
fetchData()
return () => { props.spotifyAPI.cancelRequest() }
}, [])
return (
<ArtistProfileContainer>
<AlbumContainer>
{artistData ? artistData.artistAlbums.items.map(album => {
return (
<AlbumTag
image={album.images[0].url}
name={album.name}
artists={album.artists}
key={album.id}
/>
)
})
: null}
</AlbumContainer>
</ArtistProfileContainer>
)
}
Edit:
In my api file I added an AbortController() and used a signal so I can cancel a request.
export function spotifyAPI() {
const controller = new AbortController()
const signal = controller.signal
// code ...
this.getArtist = (id) => {
return (
fetch(
`https://api.spotify.com/v1/artists/${id}`, {
headers: {"Authorization": "Bearer " + this.user_token}
}, {signal})
.then(response => {
return checkServerStat(response.status, response.json())
})
)
}
// code ...
// this is my cancel method
this.cancelRequest = () => controller.abort()
}
My spotify.getArtistProfile() looks like this
this.getArtistProfile = (id,includeGroups,market,limit,offset) => {
return Promise.all([
this.getArtist(id),
this.getArtistAlbums(id,includeGroups,market,limit,offset),
this.getArtistTopTracks(id,market)
])
.then(response => {
return ({
artist: response[0],
artistAlbums: response[1],
artistTopTracks: response[2]
})
})
}
but because my signal is used for individual api calls that are resolved in a Promise.all I can't abort() that promise so I will always be setting the state.
For me, clean the state in the unmount of the component helped.
const [state, setState] = useState({});
useEffect(() => {
myFunction();
return () => {
setState({}); // This worked for me
};
}, []);
const myFunction = () => {
setState({
name: 'Jhon',
surname: 'Doe',
})
}
Sharing the AbortController between the fetch() requests is the right approach.
When any of the Promises are aborted, Promise.all() will reject with AbortError:
function Component(props) {
const [fetched, setFetched] = React.useState(false);
React.useEffect(() => {
const ac = new AbortController();
Promise.all([
fetch('http://placekitten.com/1000/1000', {signal: ac.signal}),
fetch('http://placekitten.com/2000/2000', {signal: ac.signal})
]).then(() => setFetched(true))
.catch(ex => console.error(ex));
return () => ac.abort(); // Abort both fetches on unmount
}, []);
return fetched;
}
const main = document.querySelector('main');
ReactDOM.render(React.createElement(Component), main);
setTimeout(() => ReactDOM.unmountComponentAtNode(main), 1); // Unmount after 1ms
<script src="//cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.development.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.development.js"></script>
<main></main>
For example, you have some component that does some asynchronous actions, then writes the result to state and displays the state content on a page:
export default function MyComponent() {
const [loading, setLoading] = useState(false);
const [someData, setSomeData] = useState({});
// ...
useEffect( () => {
(async () => {
setLoading(true);
someResponse = await doVeryLongRequest(); // it takes some time
// When request is finished:
setSomeData(someResponse.data); // (1) write data to state
setLoading(false); // (2) write some value to state
})();
}, []);
return (
<div className={loading ? "loading" : ""}>
{someData}
<Link to="SOME_LOCAL_LINK">Go away from here!</Link>
</div>
);
}
Let's say that user clicks some link when doVeryLongRequest() still executes. MyComponent is unmounted but the request is still alive and when it gets a response it tries to set state in lines (1) and (2) and tries to change the appropriate nodes in HTML. We'll get an error from subject.
We can fix it by checking whether compponent is still mounted or not. Let's create a componentMounted ref (line (3) below) and set it true. When component is unmounted we'll set it to false (line (4) below). And let's check the componentMounted variable every time we try to set state (line (5) below).
The code with fixes:
export default function MyComponent() {
const [loading, setLoading] = useState(false);
const [someData, setSomeData] = useState({});
const componentMounted = useRef(true); // (3) component is mounted
// ...
useEffect( () => {
(async () => {
setLoading(true);
someResponse = await doVeryLongRequest(); // it takes some time
// When request is finished:
if (componentMounted.current){ // (5) is component still mounted?
setSomeData(someResponse.data); // (1) write data to state
setLoading(false); // (2) write some value to state
}
return () => { // This code runs when component is unmounted
componentMounted.current = false; // (4) set it to false when we leave the page
}
})();
}, []);
return (
<div className={loading ? "loading" : ""}>
{someData}
<Link to="SOME_LOCAL_LINK">Go away from here!</Link>
</div>
);
}
Why do I keep getting this warning?
The intention of this warning is to help you prevent memory leaks in your application. If the component updates it's state after it has been unmounted from the DOM, this is an indication that there could be a memory leak, but it is an indication with a lot of false positives.
How do I know if I have a memory leak?
You have a memory leak if an object that lives longer than your component holds a reference to it, either directly or indirectly. This usually happens when you subscribe to events or changes of some kind without unsubscribing when your component unmounts from the DOM.
It typically looks like this:
useEffect(() => {
function handleChange() {
setState(store.getState())
}
// "store" lives longer than the component,
// and will hold a reference to the handleChange function.
// Preventing the component to be garbage collected after
// unmount.
store.subscribe(handleChange)
// Uncomment the line below to avoid memory leak in your component
// return () => store.unsubscribe(handleChange)
}, [])
Where store is an object that lives further up the React tree (possibly in a context provider), or in global/module scope. Another example is subscribing to events:
useEffect(() => {
function handleScroll() {
setState(window.scrollY)
}
// document is an object in global scope, and will hold a reference
// to the handleScroll function, preventing garbage collection
document.addEventListener('scroll', handleScroll)
// Uncomment the line below to avoid memory leak in your component
// return () => document.removeEventListener(handleScroll)
}, [])
Another example worth remembering is the web API setInterval, which can also cause memory leak if you forget to call clearInterval when unmounting.
But that is not what I am doing, why should I care about this warning?
React's strategy to warn whenever state updates happen after your component has unmounted creates a lot of false positives. The most common I've seen is by setting state after an asynchronous network request:
async function handleSubmit() {
setPending(true)
await post('/someapi') // component might unmount while we're waiting
setPending(false)
}
You could technically argue that this also is a memory leak, since the component isn't released immediately after it is no longer needed. If your "post" takes a long time to complete, then it will take a long time to for the memory to be released. However, this is not something you should worry about, because it will be garbage collected eventually. In these cases, you could simply ignore the warning.
But it is so annoying to see the warning, how do I remove it?
There are a lot of blogs and answers on stackoverflow suggesting to keep track of the mounted state of your component and wrap your state updates in an if-statement:
let isMountedRef = useRef(false)
useEffect(() => {
isMountedRef.current = true
return () => {
isMountedRef.current = false
}
}, [])
async function handleSubmit() {
setPending(true)
await post('/someapi')
if (!isMountedRef.current) {
setPending(false)
}
}
This is not an recommended approach! Not only does it make the code less readable and adds runtime overhead, but it might also might not work well with future features of React. It also does nothing at all about the "memory leak", the component will still live just as long as without that extra code.
The recommended way to deal with this is to either cancel the asynchronous function (with for instance the AbortController API), or to ignore it.
In fact, React dev team recognises the fact that avoiding false positives is too difficult, and has removed the warning in v18 of React.
You can try this set a state like this and check if your component mounted or not. This way you are sure that if your component is unmounted you are not trying to fetch something.
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
return () => setDidMount(false);
}, [])
if(!didMount) {
return null;
}
return (
<ArtistProfileContainer>
<AlbumContainer>
{artistData ? artistData.artistAlbums.items.map(album => {
return (
<AlbumTag
image={album.images[0].url}
name={album.name}
artists={album.artists}
key={album.id}
/>
)
})
: null}
</AlbumContainer>
</ArtistProfileContainer>
)
Hope this will help you.
I had a similar issue with a scroll to top and #CalosVallejo answer solved it :) Thank you so much!!
const ScrollToTop = () => {
const [showScroll, setShowScroll] = useState();
//------------------ solution
useEffect(() => {
checkScrollTop();
return () => {
setShowScroll({}); // This worked for me
};
}, []);
//----------------- solution
const checkScrollTop = () => {
setShowScroll(true);
};
const scrollTop = () => {
window.scrollTo({ top: 0, behavior: "smooth" });
};
window.addEventListener("scroll", checkScrollTop);
return (
<React.Fragment>
<div className="back-to-top">
<h1
className="scrollTop"
onClick={scrollTop}
style={{ display: showScroll }}
>
{" "}
Back to top <span>⟶ </span>
</h1>
</div>
</React.Fragment>
);
};
I have getting same warning, This solution Worked for me ->
useEffect(() => {
const unsubscribe = fetchData(); //subscribe
return unsubscribe; //unsubscribe
}, []);
if you have more then one fetch function then
const getData = () => {
fetch1();
fetch2();
fetch3();
}
useEffect(() => {
const unsubscribe = getData(); //subscribe
return unsubscribe; //unsubscribe
}, []);
This error occurs when u perform state update on current component after navigating to other component:
for example
axios
.post(API.BASE_URI + API.LOGIN, { email: username, password: password })
.then((res) => {
if (res.status === 200) {
dispatch(login(res.data.data)); // line#5 logging user in
setSigningIn(false); // line#6 updating some state
} else {
setSigningIn(false);
ToastAndroid.show(
"Email or Password is not correct!",
ToastAndroid.LONG
);
}
})
In above case on line#5 I'm dispatching login action which in return navigates user to the dashboard and hence login screen now gets unmounted.
Now when React Native reaches as line#6 and see there is state being updated, it yells out loud that how do I do this, the login component is there no more.
Solution:
axios
.post(API.BASE_URI + API.LOGIN, { email: username, password: password })
.then((res) => {
if (res.status === 200) {
setSigningIn(false); // line#6 updating some state -- moved this line up
dispatch(login(res.data.data)); // line#5 logging user in
} else {
setSigningIn(false);
ToastAndroid.show(
"Email or Password is not correct!",
ToastAndroid.LONG
);
}
})
Just move react state update above, move line 6 up the line 5.
Now state is being updated before navigating the user away. WIN WIN
there are many answers but I thought I could demonstrate more simply how the abort works (at least how it fixed the issue for me):
useEffect(() => {
// get abortion variables
let abortController = new AbortController();
let aborted = abortController.signal.aborted; // true || false
async function fetchResults() {
let response = await fetch(`[WEBSITE LINK]`);
let data = await response.json();
aborted = abortController.signal.aborted; // before 'if' statement check again if aborted
if (aborted === false) {
// All your 'set states' inside this kind of 'if' statement
setState(data);
}
}
fetchResults();
return () => {
abortController.abort();
};
}, [])
Other Methods:
https://medium.com/wesionary-team/how-to-fix-memory-leak-issue-in-react-js-using-hook-a5ecbf9becf8
If the user navigates away, or something else causes the component to get destroyed before the async call comes back and tries to setState on it, it will cause the error. It's generally harmless if it is, indeed, a late-finish async call. There's a couple of ways to silence the error.
If you're implementing a hook like useAsync you can declare your useStates with let instead of const, and, in the destructor returned by useEffect, set the setState function(s) to a no-op function.
export function useAsync<T, F extends IUseAsyncGettor<T>>(gettor: F, ...rest: Parameters<F>): IUseAsync<T> {
let [parameters, setParameters] = useState(rest);
if (parameters !== rest && parameters.some((_, i) => parameters[i] !== rest[i]))
setParameters(rest);
const refresh: () => void = useCallback(() => {
const promise: Promise<T | void> = gettor
.apply(null, parameters)
.then(value => setTuple([value, { isLoading: false, promise, refresh, error: undefined }]))
.catch(error => setTuple([undefined, { isLoading: false, promise, refresh, error }]));
setTuple([undefined, { isLoading: true, promise, refresh, error: undefined }]);
return promise;
}, [gettor, parameters]);
useEffect(() => {
refresh();
// and for when async finishes after user navs away //////////
return () => { setTuple = setParameters = (() => undefined) }
}, [refresh]);
let [tuple, setTuple] = useState<IUseAsync<T>>([undefined, { isLoading: true, refresh, promise: Promise.resolve() }]);
return tuple;
}
That won't work well in a component, though. There, you can wrap useState in a function which tracks mounted/unmounted, and wraps the returned setState function with the if-check.
export const MyComponent = () => {
const [numPendingPromises, setNumPendingPromises] = useUnlessUnmounted(useState(0));
// ..etc.
// imported from elsewhere ////
export function useUnlessUnmounted<T>(useStateTuple: [val: T, setVal: Dispatch<SetStateAction<T>>]): [T, Dispatch<SetStateAction<T>>] {
const [val, setVal] = useStateTuple;
const [isMounted, setIsMounted] = useState(true);
useEffect(() => () => setIsMounted(false), []);
return [val, newVal => (isMounted ? setVal(newVal) : () => void 0)];
}
You could then create a useStateAsync hook to streamline a bit.
export function useStateAsync<T>(initialState: T | (() => T)): [T, Dispatch<SetStateAction<T>>] {
return useUnlessUnmounted(useState(initialState));
}
Try to add the dependencies in useEffect:
useEffect(() => {
fetchData()
return () => { props.spotifyAPI.cancelRequest() }
}, [fetchData, props.spotifyAPI])
Usually this problem occurs when you showing the component conditionally, for example:
showModal && <Modal onClose={toggleModal}/>
You can try to do some little tricks in the Modal onClose function, like
setTimeout(onClose, 0)
This works for me :')
const [state, setState] = useState({});
useEffect( async ()=>{
let data= await props.data; // data from API too
setState(users);
},[props.data]);
I had this problem in React Native iOS and fixed it by moving my setState call into a catch. See below:
Bad code (caused the error):
const signupHandler = async (email, password) => {
setLoading(true)
try {
const token = await createUser(email, password)
authContext.authenticate(token)
} catch (error) {
Alert.alert('Error', 'Could not create user.')
}
setLoading(false) // this line was OUTSIDE the catch call and triggered an error!
}
Good code (no error):
const signupHandler = async (email, password) => {
setLoading(true)
try {
const token = await createUser(email, password)
authContext.authenticate(token)
} catch (error) {
Alert.alert('Error', 'Could not create user.')
setLoading(false) // moving this line INTO the catch call resolved the error!
}
}
Similar problem with my app, I use a useEffect to fetch some data, and then update a state with that:
useEffect(() => {
const fetchUser = async() => {
const {
data: {
queryUser
},
} = await authFetch.get(`/auth/getUser?userId=${createdBy}`);
setBlogUser(queryUser);
};
fetchUser();
return () => {
setBlogUser(null);
};
}, [_id]);
This improves upon Carlos Vallejo's answer.
useEffect(() => {
let abortController = new AbortController();
// your async action is here
return () => {
abortController.abort();
}
}, []);
in the above code, I've used AbortController to unsubscribe the effect. When the a sync action is completed, then I abort the controller and unsubscribe the effect.
it work for me ....
The easy way
let fetchingFunction= async()=>{
// fetching
}
React.useEffect(() => {
fetchingFunction();
return () => {
fetchingFunction= null
}
}, [])
options={{
filterType: "checkbox"
,
textLabels: {
body: {
noMatch: isLoading ?
:
'Sorry, there is no matching data to display',
},
},
}}

State values passed in as arguments in useEffect not being read in reactjs

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.

setInterval() keeps running even after clearing it using clearInterval()

Most of the answers I found here was to use clearInterval() inside a return statement in a useEffect(). But, still for some reasons it keeps executing.
I'm also getting the following warning in the logs :-
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. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in StartTest (at SceneView.tsx:126)
attaching the code for Reference.
const [connectionInterval, setConnectionInterval] = useState(null);
const [batteryInterval, setBatteryInterval] = useState(null);
const [heartRateInterval, setHeartRateInterval] = useState(null);
useEffect(() => {
startServices();
return () => {
clearServices();
};
}, []);
const startServices= () => {
let ctnInt = setInterval(() => checkConnection(), 5000);
setConnectionInterval(ctnInt);
let btryInt = setInterval(
() =>
Battery(value => {
setBattery(value);
}),
900000,
);
setBatteryInterval(btryInt);
let hrRtInt = setInterval(
() =>
HeartRate(
hr => {
if (finish) {
clearInterval(heartRateInterval);
}
let rate = Math.round(hr);
setHeartRate(rate);
},
onError => {
console.log('API ERROR');
},
),
3000,
);
setHeartRateInterval(hrRtInt);
};
const clearServices = () => {
clearInterval(connectionInterval);
clearInterval(batteryInterval);
clearInterval(heartRateInterval);
};```
You're not passing any deps to useEffect, so the effect functions never update, and in the version of clearServices that you call, connectionInterval and friends are all still null. See the note here.
In general I would approach setInterval like this:
useEffect(() => {
const intervalFn = () => {
console.log('interval fired')
}
const intervalId = setInterval(intervalFn, 1000)
return () => {
clearInterval(intervalId)
}
}, [])
(this version really has no deps, because everything is captured inside useEffect. but in practice you would probably have some.)
I had this issue a couple of weeks ago and what I did to stop it was to set a new state after clearing the interval.
For example. I was trying to build a countdown that ran from 30 to 0 and stop at 0. This is what I did
const [timeRemaining, setTimeRemaining] = useState(30);
useEffect(() => {
let timeLeft = timeRemaining;
let interval = setInterval(() => {
if (timeLeft === 0) {
clearInterval(interval);
setTimeRemaining(0);
} else {
setTimeRemaining((timeLeft -= 1));
}
}, 1000);
}, [timeRemaining]);
Setting the state to 0 after clearing the interval was the only way to stop the countdown timer at 0
In useEffect, you should declare dependencies in array after callback function. In the code above, startServices is dependency, because it is declared outside the useEffect.
https://reactjs.org/docs/hooks-reference.html#useeffect
You can learn about useEffect in link.

React stop timeout when useEffect prop changes

I'm building and React application, where I have to save on what page of online display document user currently is, but there is a problem, that if users scroll throughout the document, it saves all pages. So we want to use some kind of timer function, that would only trigger if prop page hasn't changed in 30 seconds for example. Here is my code, it invokes later, but still for all pages through the scroll.
useEffect(
async () => {
let timeout;
if (scriptInfo && authData && numPages) {
setTimeout(async () => {
const res = await postScriptAnalyticsData({
script_id: scriptInfo._id,
user_id: authData.user.user_id,
page: page,
full_page: numPages
});
}, 10000);
}
return () => {
clearTimeout(timeout);
};
},
[ scriptInfo, page, authData, numPages ]
);
Your useEffect() callback function should not be async. The useEffect hook should return a function when a value is returned, but if your callback is async, then it will implicitly return a Promise. Remove the async from your useEffect callback, as this isn't needed as you're not using await directly within the function. Also, assign the timeout to the return value of setTimeout() so you can clear it:
useEffect(() => { // can't be `async`, so remove it
let timeout;
if (scriptInfo && authData && numPages) {
timeout = setTimeout(async () => { // assign `timeout`
const res = await postScriptAnalyticsData({
script_id: scriptInfo._id,
user_id: authData.user.user_id,
page: page,
full_page: numPages
});
}, 10000);
}
return () => {
clearTimeout(timeout);
};
},
[scriptInfo, page, authData, numPages]
);
Declare the setTimeout() ... , out of the components cause or if it is not using hooks
And keep it in a variable
Ex:
const timerCustom = setTimeout(...)
// Component
useEffect (() => {
...
clearTimeout(timerCustom);
...
})
and just clear it when your page props changed

How do I use an updated value in useEffect

I'm new to Javascript and I'm currently experimenting with the Demo application out of the Docker getting-started tutorial. The application is a simple Todo list where you can add items and remove them.
I'm trying to update the list on every instance of the page without having to reload the page.
I've managed to edit the node express server so that it sends updates via Server-sent events.
The problem:
The frontend uses React. The data of the currently displayer items is contained in the ìtems array.
onNewItem adds items to that array. However when onNewItem is called from onmessage the items array is null even though it's not null when onNewItem is called from other React components. How can I access the initialized version of the items array? (It gets initialized by the 1. useEffect which fetches items from the server)
Below is a part of the code
function TodoListCard() {
const [items, setItems] = React.useState(null);
const [ listening, setListening ] = React.useState(false);
React.useEffect(() => {
fetch('/items')
.then(r => r.json())
.then(setItems);
}, []);
React.useEffect( () => {
if (!listening) {
const events = new EventSource('/events/subscribe');
events.onmessage = (event) => {
const parsedData = JSON.parse(event.data);
switch (parsedData.type) {
case "add":
var newItem = {id: parsedData.id, name: parsedData.name, completed: parsedData.completed};
onNewItem(newItem);
break;
default:
break;
}
};
setListening(true);
}
}, [listening]);
const onNewItem = React.useCallback(
newItem => {
if (items.some(e => e.id === newItem.id)){return;}
setItems([...items, newItem]);
},
[items],
);
Let's start of by why things are going wrong. The issue is that when you call onNewItem(newItem) you are using an outdated reference to the onNewItem. For this reason items within the function will still be set to the initial value.
You partially solved this by providing an dependency array to React.useCallback. This will update onNewItem when a new value of items is available. However since React.useEffect does not list onNewItem as a dependency it keeps using the old version of onNewItem.
With this being said you might consider adding onNewItem, to the dependency array of React.useEffect. Although this is the correct action, just adding this to dependency array is not enough.
What is the problem you get when you add onNewItem to the depency array of React.useEffect? There is no cleanup function, so you will subscribe to the channel multiple times with different onmessage handlers (different versions of onNewItem).
So taking all the above into account a solution might look something like this:
function TodoListCard() {
const [items, setItems] = React.useState(null);
const [events, setEvents] = React.useState(null);
React.useEffect(() => {
const pEvents = fetch('/items')
.then(r => r.json())
.then(setItems)
.then(() => new EventSource('/events/subscribe'));
pEvents.then(setEvents);
return () => pEvents.then(events => events.close());
}, []);
React.useEffect(() => {
if (!events) return;
events.onmessage = (event) => {
const parsedData = JSON.parse(event.data);
switch (parsedData.type) {
case "add":
var newItem = {
id: parsedData.id,
name: parsedData.name,
completed: parsedData.completed
};
onNewItem(newItem);
break;
default:
break;
}
};
}, [events, onNewItem]);
const onNewItem = React.useCallback(newItem => {
const isPresent = items.some(item => item.id === newItem.id);
if (isPresent) return;
setItems([...items, newItem]);
}, [items]);
return (
// ...
);
}
I've moved the EventSource creation inside the first React.useEffect since that only needs to happen once the component is mounted (and needs to close the connection on unmount). An empty dependency array will only call the function on mount, and calls the cleanup function on unmount.
The second React.useEffect now has the dependency array [events, onNewItem], because when events is set the onmessage handler needs to be attached. And if the onNewItem callback updates to a new version you should attach it as the new onmessage handler (replacing the old handler). This doesn't need a cleanup function anymore since, opening and closing events is already handled.
Although the above should do the job. If managing a specific state is becoming more complicated it might be better to opt for useReducer instead of useState.
function reducer(items, action) {
switch (action.type) {
case "add":
const isPresent = items.some(item => item.id == action.item.id);
if (isPresent) return items;
return [...items, action.item];
case "replace all":
return action.items;
case "complete": // <- unused example case
return items.map(item => {
if (item.id != action.id) return item;
return {...item, completed: true};
});
// ...
default: // silently ignore unsupported operations
return items;
}
}
function TodoListCard() {
const [items, dispatch] = React.useReducer(reducer, null);
React.useEffect(() => {
const pEvents = fetch('/items')
.then(r => r.json())
.then(items => dispatch({type: "replace all", items}))
.then(() => new EventSource('/events/subscribe'));
pEvents.then(events => {
events.onmessage = (event) => {
const {type, ...item} = JSON.parse(event.data);
dispatch({type, item});
};
});
return () => pEvents.then(events => events.close());
}, []);
// if you still need onNewItem for your render:
const onNewItem = React.useCallback(item => {
dispatch({type: "add", item});
}, []);
return (
// ...
);
}
The above extracts all the items management logic into a "reducer" function. The dispatch function returned by useReducer is guaranteed to be stable by React, so you can omit it from dependency arrays (but you don't have to).
The error that you have done is you are not getting any data from your api.The setItems in your first useEffect won't work.
Wrong Way:
React.useEffect(() => {
fetch('/items')
.then(r => r.json())
.then(setItems);
}, []);
Right Way:
useEffect(() => {
fetch('/items')
.then(r => r.json())
.then((result) => {
setItems(result.items)
});
}, []);

Categories

Resources