Infinte Loop in React while using navigator.geolocation.watchPosition - javascript

So I am trying to get the position of the user and store it in a React.State. I have a settings page where the user can set settings.positionAllowed to either true or false by pressing a button, false is the default value. Until pressing the button, everything works fine. But as soon as the user sets the setting to true and goes back to home page, an endless loop is created and the app crashes after some time. I first thought that the reason for this is that an instance of navigator.geolovation.watchPosition is created on every re-rendering of the component. But also the effect hook did not solve the problem.
Here is my code:
navigator.geolocation ? let posObj = {positionAllowed: false} : let posObj = {positionAllowed: true};
const [settings, setSettings] = useState(posObj);
useEffect(() => {
if(settings.positionAllowed&&navigator.geolocation){
let geolocation = navigator.geolocation.watchPosition((rawPosition)=>{
let positionData = {
positionAvailable: true,
data: rawPosition
}
if(positionData!=position){
setPosition(positionData);
}
},(err)=>{
console.log(err);
},{
enableHighAccuracy: true,
maximumAge: 10000
});
}
}
});

You need to pass [] as the second parameter of useEffect. Then, it will work like componentDidMount() in the class component. Unless, it will be called every time the state's updated, and will occur infinite loops.
navigator.geolocation ? let posObj = {positionAllowed: false} : let posObj = {positionAllowed: true};
const [settings, setSettings] = useState(posObj);
useEffect(() => {
if(settings.positionAllowed&&navigator.geolocation){
let geolocation = navigator.geolocation.watchPosition((rawPosition)=>{
let positionData = {
positionAvailable: true,
data: rawPosition
}
if(positionData!=position){
setPosition(positionData);
}
},(err)=>{
console.log(err);
},{
enableHighAccuracy: true,
maximumAge: 10000
});
}
}
}, []); // <---------------------------------

the reason for this is because your useEffect is being run every render. You need to add a dependency array. If you use a blank dependency array, it will only run on the first render.
This might help you understand it better.
[https://medium.com/better-programming/understanding-the-useeffect-dependency-array-2913da504c44][1]
navigator.geolocation ? let posObj = {positionAllowed: false} : let posObj = {positionAllowed: true};
const [settings, setSettings] = useState(posObj);
useEffect(() => {
if(settings.positionAllowed&&navigator.geolocation){
let geolocation = navigator.geolocation.watchPosition((rawPosition)=>{
let positionData = {
positionAvailable: true,
data: rawPosition
}
if(positionData!=position){
setPosition(positionData);
}
},(err)=>{
console.log(err);
},{
enableHighAccuracy: true,
maximumAge: 10000
});
}
}
}, []);

Related

setState causing infinite loop in custom hook

I've created a custom hook within my React app, but for some reason when I update the internal state via an event listener, it causes an infinite loop to be triggered (when it shouldn't). Here's my code:
// Note that this isn't a React component - just a regular JavaScript class.
class Player{
static #audio = new Audio();
static #listenersStarted = false;
static #listenerCallbacks = {
playing: [],
paused: [],
loaded: []
};
static mount(){
const loaded = () => {
this.removeListenerCallback("loaded", loaded);
};
this.addListenerCallback("loaded", loaded);
}
// This method is called on the initialization of the React
// app and is only called once. It's only purpose is to ensure
// that all of the listeners and their callbacks get fired.
static startListeners(){
const eventShorthands = {
playing: "play playing",
paused: "pause ended",
loaded: "loadedmetadata"
};
Object.keys(eventShorthands).forEach(key => {
const actualEvents = eventShorthands[key];
actualEvents.split(" ").forEach(actualEvent => {
this.#audio.addEventListener(actualEvent, e => {
const callbacks = this.#listenerCallbacks[key];
callbacks.forEach(callback => {
callback(e)
});
});
});
});
}
static addListenerCallback(event, callback){
const callbacks = this.#listenerCallbacks;
if(callbacks.hasOwnProperty(event)){
// Remember this console log
console.log(true);
this.#listenerCallbacks[event].push(callback);
}
}
static removeListenerCallback(event, callback){
const listenerCallbacks = this.#listenerCallbacks;
if(listenerCallbacks.hasOwnProperty(event)){
const index = listenerCallbacks[event].indexOf(callback);
this.#listenerCallbacks[event].splice(index, 1);
}
}
}
const usePlayer = (slug) => {
// State setup
const [state, setState] = useReducer(
(state, newState) => ({ ...state, ...newState }), {
mounted: false,
animationRunning: false,
allowNextFrame: false
}
);
const _handleLoadedMetadata = () => {
// If I remove this _stopAnimation, the console log mentioned
// in the player class only logs true to the console 5 times.
// Whereas if I keep it, it will log true infinitely.
_stopAnimation();
};
const _stopAnimation = () => {
setState({
allowNextFrame: false,
animationRunning: false
});
}
useEffect(() => {
Player.addListenerCallback("loaded", _handleLoadedMetadata);
return () => {
Player.removeListenerCallback("loaded", _handleLoadedMetadata);
};
}, []);
return {
mounted: state.mounted
};
};
This makes me think that the component keeps on re-rendering and calling Player.addListenerCallback(), but the strange thing is, if I put a console.log(true) within the useEffect() at the end, it'll only output it twice.
All help is appreciated, cheers.
When you're hooking (pun unintended) up inner functions in React components (or hooks) to external event handlers, you'll want to be mindful of the fact that the inner function's identity changes on every render unless you use useCallback() (which is a specialization of useMemo) to guide React to keep a reference to it between renders.
Here's a small simplification/refactoring of your code that seems to work with no infinite loops.
instead of a class with only static members, Player is a regular class of which there is an app-wide singletonesque instance.
instead of hooking up separate event listeners for each event, the often-overlooked handleEvent protocol for addEventListener is used
the hook event listener callback is now properly useCallbacked.
the hook event listener callback is responsible for looking at the event.type field to figure out what's happening.
the useEffect now properly has the ref to the callback it registers/unregisters, so if the identity of the callback does change, it gets properly re-registered.
I wasn't sure what the state in your hook was used for, so it's not here (but I'd recommend three separate state atoms instead of (ab)using useDispatch for an object state if possible).
The same code is here in a Codesandbox (with a base64-encoded example mp3 that I didn't care to add here for brevity).
const SMALL_MP3 = "https://...";
class Player {
#audio = new Audio();
#eventListeners = [];
constructor() {
["play", "playing", "pause", "ended", "loadedmetadata", "canplay"].forEach((event) => {
this.#audio.addEventListener(event, this);
});
}
play(src) {
if (!this.#audio.parentNode) {
document.body.appendChild(this.#audio);
}
this.#audio.src = src;
}
handleEvent = (event) => {
this.#eventListeners.forEach((listener) => listener(event));
};
addListenerCallback(callback) {
this.#eventListeners.push(callback);
}
removeListenerCallback(callback) {
this.#eventListeners = this.#eventListeners.filter((c) => c !== callback);
}
}
const player = new Player();
const usePlayer = (slug) => {
const eventHandler = React.useCallback(
(event) => {
console.log("slug:", slug, "event:", event.type);
},
[slug],
);
React.useEffect(() => {
player.addListenerCallback(eventHandler);
return () => player.removeListenerCallback(eventHandler);
}, [eventHandler]);
};
export default function App() {
usePlayer("floop");
const handlePlay = React.useCallback(() => {
player.play(SMALL_MP3);
}, []);
return (
<div className="App">
<button onClick={handlePlay}>Set player source</button>
</div>
);
}
The output, when one clicks on the button, is
slug: floop event: loadedmetadata
slug: floop event: canplay

Infinitegrid duplicates API calls

I use vue-infinitegrid and I have realized in a browser that a backend API is called three times. Some code first (git):
<GridLayout
ref="ig"
:options="options"
:layoutOptions="layoutOptions"
#append="onAppend"
#layout-complete="onLayoutComplete"
#image-error="onImageError"
>
<div slot="loading">Loading...</div>
<div class="item" v-for="(item) in list" :key="item.key">
<ViewItem :item="item"/>
</div>
</GridLayout>
data() {
return {
start: 0,
loading: false,
list: [],
isEnded: false,
options: {
isOverflowScroll: false,
useFit: true,
useRecycle: true,
horizontal: false,
align: 'center',
transitionDuration: 0.2,
},
layoutOptions: {
margin: 15,
align: 'center',
},
pageSize: 3,
};
},
methods: {
async onAppend({ groupKey, startLoading }) {
this.$log.debug(`onAppend group key = ${groupKey}`);
const { list } = this;
if (this.isEnded) return;
const items = await this.loadItems();
startLoading();
this.list = list.concat(items);
},
async loadItems() {
const start = this.start || 0, size = parseFloat(this.pageSize), { tag } = this;
this.$log.debug(`loadItems start = ${start}, size = ${size}`);
let res = await this.$store.dispatch('GET_ITEM_STREAM', { start, size, tag });
if (res.length === 0) { // todo or smaller than requested
this.$log.debug('loadItems no data');
this.isEnded = true;
this.$refs.ig.endLoading();
return res;
}
if (this.exceptItem) {
res = res.filter(item => item._id !== this.exceptItem._id);
}
this.start = start + res.length;
this.$log.debug('loadItems finished');
return res;
},
onLayoutComplete({ isLayout, endLoading }) {
this.$log.debug(`onLayoutComplete isLayout = ${isLayout}`);
if (!isLayout) {
endLoading();
}
},
And some logs:
onAppend group key =
ItemList.vue:71 loadItems start = 0, size = 3
items.js:132 GET_ITEM_STREAM {"start":0,"size":3}
See more tips at https://vuejs.org/guide/deployment.html
ItemList.vue:83 loadItems finished
ItemList.vue:87 onLayoutComplete isLayout = false
ItemList.vue:62 onAppend group key =
ItemList.vue:71 loadItems start = 3, size = 3
items.js:132 GET_ITEM_STREAM {"start":3,"size":3}
ItemList.vue:62 onAppend group key =
ItemList.vue:71 loadItems start = 3, size = 3
items.js:132 GET_ITEM_STREAM {"start":3,"size":3}
2 ItemList.vue:83 loadItems finished
ItemList.vue:87 onLayoutComplete isLayout = false
ItemList.vue:62 onAppend group key =
ItemList.vue:71 loadItems start = 6, size = 3
items.js:132 GET_ITEM_STREAM {"start":6,"size":3}
ItemList.vue:62 onAppend group key =
ItemList.vue:71 loadItems start = 6, size = 3
items.js:132 GET_ITEM_STREAM {"start":6,"size":3}
2 ItemList.vue:83 loadItems finished
ItemList.vue:87 onLayoutComplete isLayout = false
I can see that start is incremented after onAppend is called. It looks like some concurrency issue, that the infinitegrid component does not wait until the REST call is finished and fires new event. Has anybody any experience with this component and knows how to handle this situation when I need to wait for a backend response?
Update
I have replaced async call with fixed data and it started to work correctly. So the trouble is with async.
// let items = await this.$store.dispatch('GET_ITEM_STREAM', { start, size, tag });
let items = [{ ...
Update:
Code sandbox with minimum reproducible scenerio: https://w56p2.csb.app/
The symptoms are different now, probably exhibiting the root cause - the event is emitted before the previous is processed.
https://github.com/naver/egjs-infinitegrid/issues/365
https://naver.github.io/egjs-infinitegrid/storybook/?path=/story/loading-bar-with-data-delay--grid-layout
In startLoading and endLoading, the loading bar appears and disappears, and some functions are temporarily disabled (moveTo, useFit).
The append and prepend work and must be prevented through the isProcessing method.
onAppend({ groupKey, startLoading, currentTarget }) {
if (currentTarget.isProcessing()) {
return;
}
}

How to set a state to refresh a google-map-content automatically

I built a small boat visualizer. I am using AISHub APIs. After fetching data from the APIs I am able to obtain a json file with the vessels I am interested in and inject these vessels inside a table.
The user has to manually update the page pushing the refresh button on top left of the page to see the new updated table.
The problem: How to set a state to refresh the google-map content automatically every minute instead of the user doing it manually?
Below the code:
GoogleMap.js
class BoatMap extends Component {
constructor(props) {
super(props);
this.state = {
buttonEnabled: true,
buttonClickedAt: null,
progress: 0,
ships: [],
type: 'All',
shipTypes: [],
activeShipTypes: [],
logoMap: {}
};
this.updateRequest = this.updateRequest.bind(this);
this.countDownInterval = null;
}
async componentDidMount() {
this.countDownInterval = setInterval(() => {
if (!this.state.buttonClickedAt) return;
const date = new Date();
const diff = Math.floor((date.getTime() - this.state.buttonClickedAt.getTime()) / 1000);
if (diff < 90) {
this.setState({
progress: diff,
buttonEnabled: false
});
} else {
this.setState({
progress: 0,
buttonClickedAt: null,
buttonEnabled: true
});
}
}, 500);
await this.updateRequest();
const shipTypeResults = await Client.getEntries({
content_type: 'competitors'
});
console.log(shipTypeResults);
const shipTypes = shipTypeResults.items.map((data) => data.fields);
const logoMap = shipTypes.reduce((acc, type) => {
return {
...acc,
[type.name]: type.images.fields.file.url
};
}, {});
console.log({ shipTypes });
this.setState({
logoMap
});
}
componentDidUpdate(prevProps, prevState) {
if (this.state.type !== prevState.type) {
}
}
componentWillUnmount() {
clearInterval(this.countdownInterval);
}
async updateRequest() {
const url = 'http://localhost:3001/hello';
console.log(url);
const fetchingData = await fetch(url);
const ships = await fetchingData.json();
console.log(ships);
this.setState({
buttonEnabled: false,
buttonClickedAt: new Date(),
progress: 0,
ships
});
setTimeout(() => {
this.setState({ buttonEnabled: true });
});
}
render() {
return (
<div className="google-map">
<GoogleMapReact
bootstrapURLKeys={{ key: 'KEY' }}
center={{
lat: this.props.activeShip ? this.props.activeShip.latitude : 42.4,
lng: this.props.activeShip ? this.props.activeShip.longitude : -71.1
}}
zoom={8}
>
</GoogleMapReact>
</div>
);
}
}
What I have done so far:
A good way would be using a setTimeout() but would that be correct? Where should that be applied and how?
setTimeout(function () {
location.reload();
}, 60 * 1000);
Or maybe setting an interval as a refresh rate?
I am a bit confused on what would the best way to approach this.
On your request function i guess u want to disable the button while the api doesn't return, so maybe move this piece above the requests:
this.setState({
buttonEnabled: false,
buttonClickedAt: new Date(),
progress: 0,
ships
});
If im wrong you could remove the timeout from the second setState and call as a callback on the first like this:
this.setState({
buttonEnabled: false,
buttonClickedAt: new Date(),
progress: 0,
ships
}, () => {
this.setState({ buttonEnabled: true });
});
on the last part instead of location.reload() set a interval calling the update on ur componentDidMount:
let updateInterval = setInterval(() => {
this.updateRequest();
}, 60 * 1000);
this.setState({updateInterval})
then on the componentWillUnmount you clear the interval this.state.updateInterval

React-native slider not saving current value, resets to default value not new value

So I have a component that updates all fields except the slider does not update it returns to default value instead. I need to keep the slider in place. The user presses an update button and the date and time update to current times but the slider reverts to a value of '5'.
I have tried playing with moment.js but the best I can get is that it will set the slider to a value of today. So instead of setting it to days until due it sets the value on update to todays date, so the slider value becomes 9 for August 9th, or 10 for August 10th.
this.setState({ saving: true });
greenhouse.grow_cycles[0].logs.push({
type: log.type,
value: null,
user: userId,
alert_interval: isMoment().AlertInterval,
date_started: moment().toISOString(),
date_due: moment().add(5, 'days').toISOString()
});
const res = await api.put(`/greenhouses/${greenhouse._id}`, greenhouse, token);
if (res && !res.error) {
await refresh();
let index = 1;
let actions = [
NavigationActions.navigate({ routeName: 'Home' }),
NavigationActions.navigate({ routeName: 'Greenhouse', params: { greenhouse: res.data } })
];
const resetAction = StackActions.reset({
index,
actions,
});
this.props.navigation.dispatch(resetAction);
showToast(`New ${log.type} Log Added!`);
this.setState({ saving: false });
}
else {
if (res.status === 401) {
reset();
} else {
showToast(res.error);
}
this.setState({ saving: false });
}
}```

Changing Value of State dynamically in React

I have a react component with following state
state = {
SandD: true,
Cancellation: false,
Payments: false,
EandR: false,
InternationalShipping: false,
ExpressDelievery: false
}
I want at a time only one selected state to be true and rest to be false. for that I was thinking the following logic
currentActiveState = (value) => {
for ( i=0; i<this.state.length; i++) {
console.log(this.state[i])
if(this.state[i] == value) {
console.log(this.state[i])
} else {
this.setState(this.state[i]: false)
}
}
}
Here value is the property of the state which need to have a value and rest everything should be false..
For example if Payments is true then everything else should be false..
Can someone help me in fixing the above logic or tell me how I can solve it?
Try giving this a shot:
currentActiveState = (value) => {
const newState = {};
Object.keys(this.state).forEach(key => {
newState[key] = key === value;
})
this.setState(newState);
}

Categories