How to stop setInterval when tab is not active while using useEffect? - javascript

I'm using setInterval in useEffect. When use not actively using tab, I't requesting like forever. It causing some memory issue. I have to, fetch data in every 3000ms, also stop it when user is not using this tab actively. How can I do such a thing?
I tried to use document.visibiltyState and I couldn't worked it.
My code:
useEffect(() => {
try {
const interval = setInterval(() => {
getTransactionGroupStats()
getTransactionGroups()
}, 3000)
getBinanceBalanceStats()
return () => {
clearInterval(interval)
}
} catch (error) {
console.error(error)
}
}, [])

Another alternative and a little more scalable could be that you create a custom hook to see if the user is active or not and every time it changes you execute the useEffect
useActive.ts
export const useActive = (time: number) => {
const [active, setActive] = useState(false)
const timer: any = useRef()
const events = ['keypress', 'mousemove', 'touchmove', 'click', 'scroll']
useEffect(() => {
const handleEvent = () => {
setActive(true)
if (timer.current) {
window.clearTimeout(timer.current)
}
timer.current = window.setTimeout(() => {
setActive(false)
}, time)
}
events.forEach((event: string) => document.addEventListener(event, handleEvent))
return () => {
events.forEach((event: string) => document.removeEventListener(event, handleEvent))
}
}, [time])
return active
}
YourApp.tsx
const active = useActive(3000)
useEffect(() => {
if(active){
try {
const interval = setInterval(() => {
getTransactionGroupStats()
getTransactionGroups()
}, 3000)
getBinanceBalanceStats()
return () => {
clearInterval(interval)
}
} catch (error) {
console.error(error)
}
}
}, [active])

I solved with this approach: https://blog.sethcorker.com/harnessing-the-page-visibility-api-with-react/
export function usePageVisibility () {
const [isVisible, setIsVisible] = useState(!document.hidden)
const onVisibilityChange = () => setIsVisible(!document.hidden)
React.useEffect(() => {
document.addEventListener('visibilitychange', onVisibilityChange, false)
return () => {
document.removeEventListener('visibilitychange', onVisibilityChange)
}
})
return isVisible
}

Related

why my code calling API for multiple time instedof just once after delaying of 500 ms using debounce

I'm trying to call API using debounce but in this case, API calling for every character,
for example, I type hello in search then it calls for he, hel, hell, and hello but I want only for final word hello
useEffect(() => {
updateDebounceWord(word);
}, [word]);
const updateDebounceWord = debounce(() => {
{
word.length > 1 && dictionaryApi();
}
});
function debounce(cb, delay = 500) {
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
cb(...args);
}, delay);
};
}
const dictionaryApi = async () => {
// inital state []
console.log("hited")
try {
const data = await axios.get(
`https://api.dictionaryapi.dev/api/v2/entries/${category}/${word}`
);
console.log("Fetched",word);
setMeanings(data.data);
} catch (e) {
console.log("error||", e);
}
};
In addition to Dilshans explanation, I wan't to suggest making a hook out of your debounce function, so you can easily reuse it:
const useDebounce = (cb, delay = 500) => {
const timer = useRef();
// this cleans up any remaining timeout when the hooks lifecycle ends
useEffect(() => () => clearTimeout(timer.current), [cb, delay]);
return useCallback(
(...args) => {
clearTimeout(timer.current);
timer.current = setTimeout(() => {
cb(...args);
}, delay);
},
[cb, delay]
);
};
use it like this in your components:
const updateDebounceWord = useDebounce((word) => {
console.log("api call here", word);
});
useEffect(() => {
updateDebounceWord(word);
}, [word, updateDebounceWord]);
You are using the debounce on render phase of the component. so each time when the component rebuild a new tree due to the state update, the updateDebounceWord will redeclare. Both current and workInProgress node of the component will not share any data. If you want to share the data between current and workInProgress tree use useRef or else put in global scope
A quick fix is, put the timer variable in global scope.
// keep this on global scope
let timer = null;
function debounce(cb, delay = 500) {
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
cb(...args);
}, delay);
};
}
export default function App() {
const [word, setWord] = useState("");
const sendReq = debounce((keyword) => {
apiReq(keyword);
})
useEffect(() => {
if (word.length > 0) {
sendReq(word);
}
}, [word, sendReq])
const apiReq = (keyword) => {
console.log('reached', keyword);
}
return (
<div className="App">
<input value={word} onChange={(e) => setWord(e.target.value)} />
</div>
);
}
Also put all the dependencies in the useEffect dep array otherwise it may not work as expected.
useEffect(() => {
updateDebounceWord(word);
}, [word, updateDebounceWord]);

Cleaning Function not Working in UseEffect Hook During Component Unmount in React Native

In react native app, there are two components A and B in App.js which toggle with k state change.
App.js
...
const App = () => {
const [ k, setK ] = useState(false);
const toggleK = () => setK(!k);
if(k) {
return <A />;
}else {
return <B toggleK={toggleK} />;
}
};
...
In A, setInterval is initialized in useffect. It calls async function every 10 seconds. But when it unmounts on K state change in App.js, the cleaning function is not run (no A unmounting... is logged) and so does the clearInterval.
Any thing I'm doing wrong here?
...
const A = () => {
const [ someState, setSomeState ] = useState(...);
let timer;
useEffect(() => {
if(!timer) {
timer = setInterval(async () => await run_async_func(), 10000);
}
return () => {
console.log('A unmounting...');
clearInterval(timer);
};
}, [ someState ]);
};
...
Try this with useRef().
const A = () => {
const timer = useRef()
const [ someState, setSomeState ] = useState(...);
useEffect(() => {
if(!timer) {
timer.current = setInterval(async () => await run_async_func(), 10000);
}
return () => {
console.log('A unmounting...');
clearInterval(timer.current);
};
}, [ someState ]);
};
Resolved the issue
What was causing the issue?
A. Overlooked the someState in useEffect dependency array. Basically, cleaner function will only run if any variable in dependency array mutates. So, I used another useEffect without any dependencies (see below). Thanks #TayyabMazhar for pointing out this
...
const A = () => {
const [ someState, setSomeState ] = useState(...);
let timer;
useEffect(() => {
if(!timer) {
timer = setInterval(async () => await run_async_func(), 10000);
}
}, [ someState ]);
useEffect(() => {
return () => {
console.log('A unmounting...');
clearInterval(timer);
};
}, []);
};
...

How to run a function in React when the Escape button has been pressed?

The whole idea of the component is that the first time the Escape key is pressed, the value confirm in state changes to true to verify that the user really wants to execute the secPress() function and pressing the Escape key again within 5 seconds run the secPress() function.
Unfortunately, the secPress() function in the component never run.
How to get the function to run after the second key press?
import React, { useState, useEffect, useCallback } from "react";
export function useKeypress(key, action) {
useEffect(() => {
function onKeyup(e) {
if (e.key === key) action();
}
window.addEventListener("keyup", onKeyup);
return () => window.removeEventListener("keyup", onKeyup);
}, []);
}
const App = () => {
const [confirm, setConfirm] = useState(false);
// this function never run
const secPress = () => {
console.log("Double escape pressed");
};
const _confirm = useCallback(() => {
setConfirm((prev) => !prev);
console.log(confirm); // <-- always show false
if (confirm) {
secPress();
}
}, [confirm]);
useKeypress("Escape", _confirm);
useEffect(() => {
const d = setTimeout(() => {
setConfirm(false);
}, 5000);
return () => {
clearTimeout(d);
};
}, [confirm]);
return <>{confirm ? "true" : "false"}</>;
};
export default App;
React version: 17.0.2
In your case, it's because you're passing an empty dependencies array to the useEffect inside the useKeypress function; this way, the event listener has always the same callback.
The easiest change would be to pass the dependencies, for example:
import React, { useState, useEffect, useCallback } from "react";
export function useKeypress(key, action, deps) {
useEffect(() => {
function onKeyup(e) {
if (e.key === key) action();
}
window.addEventListener("keyup", onKeyup);
return () => window.removeEventListener("keyup", onKeyup);
}, deps);
}
const App = () => {
const [confirm, setConfirm] = useState(false);
// this function never run
const secPress = () => {
console.log("Double escape pressed");
};
const _confirm = useCallback(() => {
setConfirm(prev => !prev);
console.log(confirm); // <-- always show false
if (confirm) {
secPress();
}
}, [confirm]);
useKeypress("Escape", _confirm, [confirm]);
useEffect(() => {
const d = setTimeout(() => {
setConfirm(false);
}, 5000);
return () => {
clearTimeout(d);
};
}, [confirm]);
return <>{confirm ? "true" : "false"}</>;
};
export default App;
The previous answer is incorrect, you shouldn't have to add [confirm] to the event listener effect
working example:
https://codesandbox.io/s/00rxv
const [stage, setStage] = useState("initial");
const secPress = () => {
console.log("Double escape pressed");
};
const keydown = (e) => {
if (e.key === "Escape") {
setStage((current) => (current === "initial" ? "confirm" : "success"));
}
};
useEffect(() => {
window.addEventListener("keydown", keydown);
return () => window.removeEventListener("keydown", keydown);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (stage === "success") {
secPress();
}
if (stage !== "confirm") {
return;
}
const d = setTimeout(() => {
setStage("initial");
}, 2000);
return () => {
clearTimeout(d);
};
}, [stage]);

Cleaning component states useEffect

I have states :
const { id } = useParams<IRouterParams>();
const [posts, setPosts] = useState<IPost[]>([]);
const [perPage, setPerPage] = useState(5);
const [fetchError, setFetchError] = useState("");
const [lastPostDate, setLastPostDate] = useState<string | null>(null);
// is any more posts in database
const [hasMore, setHasMore] = useState(true);
and useEffect :
// getting posts from server with first render
useEffect(() => {
console.log(posts);
fetchPosts();
console.log(hasMore, lastPostDate);
return () => {
setHasMore(true);
setLastPostDate(null);
setPosts([]);
mounted = false;
return;
};
}, [id]);
When component change (by id), I would like to clean/reset all states.
My problem is that all states are still the same, this setState functions in useEffect cleaning function doesn't work.
##UPDATE
// getting posts from server
const fetchPosts = () => {
let url;
if (lastPostDate)
url = `http://localhost:5000/api/posts/getPosts/profile/${id}?limit=${perPage}&date=${lastPostDate}`;
else
url = `http://localhost:5000/api/posts/getPosts/profile/${id}?limit=${perPage}`;
api
.get(url, {
headers: authenticationHeader(),
})
.then((resp) => {
if (mounted) {
if (resp.data.length === 0) {
setFetchError("");
setHasMore(false);
setPosts(resp.data);
return;
}
setPosts((prevState) => [...prevState, ...resp.data]);
if (resp.data.length < perPage) setHasMore(false);
setLastPostDate(resp.data[resp.data.length - 1].created_at);
setFetchError("");
}
})
.catch((err) => setFetchError("Problem z pobraniem postów."));
};
if your component isnt unmounted, then the return function inside useEffect will not be called.
if only the "id" changes, then try doing this instead:
useEffect(() => {
// ... other stuff
setHasMore(true);
setLastPostDate(null);
setPosts([]);
return () => { //...code to run on unmount }
},[id]);
whenever id changes, the codes inside useEffect will run. thus clearing out your states.
OK, I fixed it, don't know if it is the best solution, but works...
useEffect(() => {
setPosts([]);
setHasMore(true);
setLastPostDate(null);
return () => {
mounted = false;
return;
};
}, [id]);
// getting posts from server with first render
useEffect(() => {
console.log(lastPostDate, hasMore);
hasMore && !lastPostDate && fetchPosts();
}, [lastPostDate, hasMore]);

Condition inside setInterval in functional component

I set an interval inside useEffect to update data every 33 seconds if a state variable can_update is true.
The initial value for can_pdate = true. The problem is, even if I change can_update to false (using disable_update function), in the update_groups function it still comes as true.
const [can_update, set_can_update] = useState(true);
const [groups, set_groups] = useState([]);
const intervalRef = useRef();
useEffect(() => {
update_groups();
const update_interval = setInterval(() => {
update_groups();
}, 33000);
intervalRef.current = update_interval;
return () => {
clearInterval(intervalRef.current);
};
}, [project_data.id]);
const update_groups = () => {
if (can_update) {
UI.get(`/project/${project_data.id}/controllers/`).then(
(data) => {
set_groups(data.groups);
},
(error) => {
console.log("Не удалось загрузить список групп");
},
);
}
};
const enable_update = () => {
set_can_update(true);
};
const disable_update = () => {
set_can_update(false);
};
I've tried moving condition into
setInterval: `const update_interval = setInterval(() => {
if (can_update){ update_groups()};
}
and replacing setInterval for recursive setTimeout. No changes.
I've had somewhat similar code inside a class component, and there didn't seem to be any problems like this.
You need add can_update to useEffect deps, otherwise
all values
from the component scope (such as props and state) that change over
time and that are used by the effect.
https://reactjs.org/docs/hooks-effect.html
In your case useEffect was called once, inside it every 33 seconds a function update_groups was called with scoped value can_update = true.
React.useEffect(() => {
if (can_update) {
update_groups();
const update_interval = setInterval(() => {
update_groups();
}, 33000);
intervalRef.current = update_interval;
return () => {
clearInterval(intervalRef.current);
};
}
}, [project_data.id, can_update]);
const update_groups = () => {
UI.get(`/project/${project_data.id}/controllers/`).then(
data => {
set_groups(data.groups);
},
error => {
console.log('Не удалось загрузить список групп');
},
);
};

Categories

Resources