How to resolve "React Hook useEffect has a missing dependency: 'currentPosition'" - reactjs

When I include currentPosition in the useEffect dependency array or when I delete it, the code turns into an infinite loop. Why?
I have the same problem with map but when I place map in the dependency array it's ok.
import { useState, useEffect } from "react";
import { useMap } from "react-leaflet";
import L from "leaflet";
import icon from "./../constants/userIcon";
const UserMarker = () => {
const map = useMap();
const [currentPosition, setCurrentPosition] = useState([
48.856614,
2.3522219,
]);
useEffect(() => {
if (navigator.geolocation) {
let latlng = currentPosition;
const marker = L.marker(latlng, { icon })
.addTo(map)
.bindPopup("Vous êtes ici.");
map.panTo(latlng);
navigator.geolocation.getCurrentPosition(function (position) {
const pos = [position.coords.latitude, position.coords.longitude];
setCurrentPosition(pos);
marker.setLatLng(pos);
map.panTo(pos);
});
} else {
alert("Problème lors de la géolocalisation.");
}
}, [map]);
return null;
};
export default UserMarker;

The comment from DCTID explains the reason why including the state in the useEffect hook creates an infinite loop.
You need to make sure that this does not happen! You have two options:
add a ignore comment and leave it as it is
create a additional redundant variable to store the current value of the variable currentPosition and only execute the function if the value actually changed
An implementation of the second approach:
let currentPosition_store = [48.856614, 2.3522219];
useEffect(() => {
if (!hasCurrentPositionChanged()) {
return;
}
currentPosition_store = currentPosition;
// remaining function
function hasCurrentPositionChanged() {
if (currentPosition[0] === currentPosition_store[0] &&
currentPosition[1] === currentPosition_store[1]
) {
return false;
}
return true;
}
}, [map, currentPosition]);

Thank you, i have resolved the conflict how this:
import { useEffect } from "react";
import { useMap } from "react-leaflet";
import L from "leaflet";
import icon from "./../constants/userIcon";
const UserMarker = () => {
const map = useMap();
useEffect(() => {
const marker = L.marker;
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function (position) {
const latlng = [position.coords.latitude, position.coords.longitude];
marker(latlng, { icon })
.setLatLng(latlng)
.addTo(map)
.bindPopup("Vous êtes ici.");
map.panTo(latlng);
});
} else {
alert("Problème lors de la géolocalisation.");
}
}, [map]);
return null;
};
export default UserMarker;

the reason why you are getting infinite loop if currentPosition inside dependency array:
const [currentPosition, setCurrentPosition] = useState([
48.856614,
2.3522219,
]);
you have initially have a value for currentPosition and, then you are changing inside useEffect, that causes your component rerender, and this is happening infinitely. You should not add it to the dependency array.
The reason you are getting "missing-dependency warning" is,if any variable that you are using inside useEffect is defined inside that component or passed to the component as a prop, you have to add it to the dependency array, otherwise react warns you. That's why you should add map to the array and since you are not changing it inside useEffect it does not cause rerendering.
In this case you have to tell es-lint dont show me that warning by adding this://eslint-disable-next-line react-hooks/exhaustive-deps because you know what you are doing:
useEffect(() => {
if (navigator.geolocation) {
let latlng = currentPosition;
const marker = L.marker(latlng, { icon })
.addTo(map)
.bindPopup("Vous êtes ici.");
map.panTo(latlng);
navigator.geolocation.getCurrentPosition(function (position) {
const pos = [position.coords.latitude, position.coords.longitude];
setCurrentPosition(pos);
marker.setLatLng(pos);
map.panTo(pos);
});
} else {
alert("Problème lors de la géolocalisation.");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [map]);
that comment will turn of the dependency check on that line of code.

To make it easy to understand I will point out the reason first then come to solution.
Why? I have the same problem with map but when I place map in the dependency array it's ok.
Answer: The reason is useEffect is re-run based on it dependencies. useEffect first run when Component render -> component re-render (cuz it's props change...) -> useEffect will shallow compare and re-run if its dependencies change.
In your case, map Leaflet Map I bet react-leaflet will return same Map instance (same reference) if your component simply re-render -> when you component re-render -> map (Leaflet Map instance) don't change -> useEffect not re-run -> infinity loop not happen.
currentPosition is your local state and you update it inside your useEffect setCurrentPosition(pos); -> component re-render -> currentPosition in dependencies change (currentPosition is different in shallow compare) -> useEffect re-run -> setCurrentPosition(pos); make component re-render -> infinity loop
Solution:
There are some solutions:
Disable the lint rule by add // eslint-disable-next-line exhaustive-deps right above the dependencies line. But this is not recommended at all. By doing this we break how useEffect work.
Split up your useEffect:
import { useState, useEffect } from "react";
import { useMap } from "react-leaflet";
import L from "leaflet";
import icon from "./../constants/userIcon";
const UserMarker = () => {
const map = useMap();
const [currentPosition, setCurrentPosition] = useState([
48.856614,
2.3522219,
]);
// They are independent logic so we can split it yo
useEffect(() => {
if (navigator.geolocation) {
let latlng = currentPosition;
const marker = L.marker(latlng, { icon })
.addTo(map)
.bindPopup("Vous êtes ici.");
map.panTo(latlng);
} else {
alert("Problème lors de la géolocalisation.");
}
}, [map, currentPosition]);
useEffect(() => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function (position) {
const pos = [position.coords.latitude, position.coords.longitude];
setCurrentPosition(pos);
marker.setLatLng(pos);
map.panTo(pos);
});
}
}, [map]);
return null;
};
export default UserMarker;
There is a great article about useEffect from Dan, it's worth to check it out: https://overreacted.io/a-complete-guide-to-useeffect/#dont-lie-to-react-about-dependencies

Related

Updating state without rendering the whole React component (useState)

I have a component that instantiates a few classes from the Tone.js library (e.g audio players and filters) and defines a few functions acting on these objects, which are used as callbacks in a set of UI-rendered buttons (see relevant code below).
Two of these buttons are supposed to toggle the boolean state is3D using the useState hook (in the updateSpatial function) in order to enable/disable one of these buttons. However, this update obviously causes the component to re-render entirely, thus re-instantiating my classes, which prevent the defined functions to work afterwards.
In contrast, I also tried the useRef hook, which allows for is3D update without re-rendering, but the button's disabled state is not updated as the component does not re-render.
Is there a pattern that fits with this situation? My next attempts include using HOC, Context, or Redux, but I'm not sure this is the most straighforward. Thanks!
import React, {useState, useEffect, Fragment} from 'react'
import { Player, Destination} from "tone";
const Eagles = () => {
const [is3D, setIs3D] = useState(false);
useEffect(() => {
// connect players to destination, ready to play
connectBase();
});
// Instantiating players
const player1= new Player({
"url": "https://myapp.s3.eu-west-3.amazonaws.com/Aguabeber.m4a",
"autostart": false,
"onload": console.log("player 1 ready")
});
const player2 = new Player({
"url": "https://myapp.s3.eu-west-3.amazonaws.com/Aguas.m4a",
"autostart": false,
"onload": console.log("player 2 ready")
});
// Functions
function connectBase() {
player1.disconnect();
player1.connect(Destination);
player2.disconnect();
player2.chain(Destination);
}
function playStudio() {
player1.volume.value = 0;
player2.volume.value = -60;
}
function playCinema() {
player1.volume.value = -60;
player2.volume.value = 0;
}
function playstereo() {
player1.volume.value = 0;
player2.volume.value = -60;
updateSpatial();
}
function playmyhifi() {
player1.volume.value = -60;
player2.volume.value = 0;
updateSpatial();
}
function start() {
player1.start();
player2.start();
playstereo();
}
function stop() {
player1.stop();
player2.stop();
console.log("stop pressed")
}
// Update state to toggle button enabled`
function updateSpatial() {
setIs3D((is3D) => !is3D);
}
return (
<Fragment>
<ButtonTL onClick={start}>Play</ButtonTL>
<ButtonTR onClick={stop}>Stop</ButtonTR>
<ButtonTL2 onClick={playstereo}>Stereo</ButtonTL2>
<ButtonTR2 onClick={playmyhifi}>3D</ButtonTR2>
<ButtonLL disabled={is3D} onClick={playStudio}>Studio</ButtonLL>
<ButtonLR onClick={playCinema}>Cinema</ButtonLR>
</Fragment>
)
}
I see two things wrong with your code.
First, everything within the "normal" body of the function will be executed on on every render. Hence, the need for states and hooks.
States allow you to keep data between renders, and hooks allow you to do a particular action upon a state change.
Second, useEffect(()=>console.log(hi)) does not have any dependencies, hence it will run on every render.
useEffect(()=>console.log(hi),[]) will execute only on the first render.
useEffect(()=>console.log(hi),[player1]) will execute when player1 changes.
useEffect(()=>console.log(hi),[player1, player2]) will execute when player1 OR player2 change.
Be careful with hooks with dependencies. If you set the state of one of the dependencies within the hook itself, it will create an infinite loop
Here is something closer to what you want:
import React, {useState, useEffect, Fragment} from 'react'
import { Player, Destination} from "tone";
const Eagles = () => {
const [is3D, setIs3D] = useState(false);
const [player1]= useState(new Player({
"url": "https://myapp.s3.eu-west-3.amazonaws.com/Aguabeber.m4a",
"autostart": false,
"onload": console.log("player 1 ready")
}));
const [player2] = useState(new Player({
"url": "https://myapp.s3.eu-west-3.amazonaws.com/Aguas.m4a",
"autostart": false,
"onload": console.log("player 2 ready")
}));
useEffect(() => {
// connect players to destination, ready to play
connectBase();
},[player1, player2]);
// Instantiating players
// Functions
function connectBase() {
player1.disconnect();
player1.connect(Destination);
player2.disconnect();
player2.chain(Destination);
}
function playStudio() {
player1.volume.value = 0;
player2.volume.value = -60;
}
function playCinema() {
player1.volume.value = -60;
player2.volume.value = 0;
}
function playstereo() {
player1.volume.value = 0;
player2.volume.value = -60;
updateSpatial();
}
function playmyhifi() {
player1.volume.value = -60;
player2.volume.value = 0;
updateSpatial();
}
function start() {
player1.start();
player2.start();
playstereo();
}
function stop() {
player1.stop();
player2.stop();
console.log("stop pressed")
}
// Update state to toggle button enabled`
function updateSpatial() {
setIs3D((is3D) => !is3D);
}
return (
<Fragment>
<ButtonTL onClick={start}>Play</ButtonTL>
<ButtonTR onClick={stop}>Stop</ButtonTR>
<ButtonTL2 onClick={playstereo}>Stereo</ButtonTL2>
<ButtonTR2 onClick={playmyhifi}>3D</ButtonTR2>
<ButtonLL disabled={is3D} onClick={playStudio}>Studio</ButtonLL>
<ButtonLR onClick={playCinema}>Cinema</ButtonLR>
</Fragment>
)
}

React local element not updating?

I have a component which has a local variable
let endOfDocument = false;
And I have a infinite scroll function in my useEffect
useEffect(() => {
const { current } = selectScroll;
current.addEventListener('scroll', () => {
if (current.scrollTop + current.clientHeight >= current.scrollHeight) {
getMoreExercises();
}
});
return () => {
//cleanup
current.removeEventListener('scroll', () => {});
};
}, []);
In my getMoreExercises function I check if we reached the last document in firebase
function getMoreExercises() {
if (!endOfDocument) {
let ref = null;
if (selectRef.current.value !== 'All') {
ref = db
.collection('exercises')
.where('targetMuscle', '==', selectRef.current.value);
} else {
ref = db.collection('exercises');
}
ref
.orderBy('average', 'desc')
.startAfter(start)
.limit(5)
.get()
.then((snapshots) => {
start = snapshots.docs[snapshots.docs.length - 1];
if (!start) endOfDocument = true; //Here
snapshots.forEach((exercise) => {
setExerciseList((prevArray) => [...prevArray, exercise.data()]);
});
});
}
}
And when I change the options to another category I handle it with a onChange method
function handleCategory() {
endOfDocument = false;
getExercises();
}
I do this so when we change categories the list will be reset and it will no longer be the end of the document. However the endOfDocument variable does not update and getMoreExercise function will always have the endOfDocument value of true once it is set to true. I cannot change it later. Any help would be greatly appreciated.
As #DevLoverUmar mentioned, that would updated properly,
but since the endOfDocument is basically never used to "render" anything, but just a state that is used in an effect, I would put it into a useRef instead to reduce unnecessary rerenders.
Assuming you are using setExerciseList as a react useState hook variable. You should use useState for endOfDocument as well as suggested by Brian Thompson in a comment.
import React,{useState} from 'react';
const [endOfDocument,setEndOfDocument] = useState(false);
function handleCategory() {
setEndOfDocument(false);
getExercises();
}

Is it possible to avoid 'eslint(react-hooks/exhaustive-deps)' error on custom React Hook with useCallback?

Take the following custom React Hook to interact with IntersectionObserver:
import { useCallback, useRef, useState } from 'react';
type IntersectionObserverResult = [(node: Element | null) => void, IntersectionObserverEntry?];
function useIntersectionObserver(options: IntersectionObserverInit): IntersectionObserverResult {
const intersectionObserver = useRef<IntersectionObserver>();
const [entry, setEntry] = useState<IntersectionObserverEntry>();
const ref = useCallback(
(node) => {
if (intersectionObserver.current) {
console.log('[useInterSectionObserver] disconnect(🔴)');
intersectionObserver.current.disconnect();
}
if (node) {
intersectionObserver.current = new IntersectionObserver((entries) => {
console.log('[useInterSectionObserver] callback(🤙)');
console.log(entries[0]);
setEntry(entries[0]);
}, options);
console.log('[useInterSectionObserver] observe(🟢)');
intersectionObserver.current.observe(node);
}
},
[options.root, options.rootMargin, options.threshold]
);
return [ref, entry];
}
export { useIntersectionObserver };
ESLint is complaining about:
React Hook useCallback has a missing dependency: 'options'. Either include it or remove the dependency array.
If I replace the dependencies array with [options], ESLint no longer complains but there's now a much bigger problem, a rendering infinite loop.
What would be the right way to implement this custom React Hook without having the eslint(react-hooks/exhaustive-deps) error showing up?
The fix to this is to destructure the properties you need from options and set them in the dependancy array. That way you don't need options and the hook only gets called when those three values change.
import { useCallback, useRef, useState } from 'react';
type IntersectionObserverResult = [(node: Element | null) => void, IntersectionObserverEntry?];
function useIntersectionObserver(options: IntersectionObserverInit): IntersectionObserverResult {
const intersectionObserver = useRef<IntersectionObserver>();
const [entry, setEntry] = useState<IntersectionObserverEntry>();
const { root, rootMargin, threshold } = options;
const ref = useCallback(
(node) => {
if (intersectionObserver.current) {
console.log('[useInterSectionObserver] disconnect(🔴)');
intersectionObserver.current.disconnect();
}
if (node) {
intersectionObserver.current = new IntersectionObserver((entries) => {
console.log('[useInterSectionObserver] callback(🤙)');
console.log(entries[0]);
setEntry(entries[0]);
}, options);
console.log('[useInterSectionObserver] observe(🟢)');
intersectionObserver.current.observe(node);
}
},
[root, rootMargin, threshold]
);
return [ref, entry];
}
export { useIntersectionObserver };
You should always provide all the necessary values in the dep array to prevent it from using the previous cached function with stale values. One option to fix your situation is to memo the options object so only a new one is being passed when it's values change instead of on every re-render:
// in parent
// this passes a new obj on every re-render
const [ref, entry] = useIntersectionObserver({ root, rootMargin, threshold });
// this will only pass a new obj if the deps change
const options = useMemo(() => ({ root, rootMargin, threshold }), [root, rootMargin, threshold]);
const [ref, entry] = useIntersectionObserver(options);

How to check my conditions on react hooks

I need check my conditions on react hooks when startup or change value
I try that by this code but I cant run without Btn
import React,{useEffect} from 'react';
import { StyleSheet, View } from 'react-native';
import * as Permissions from 'expo-permissions';
import { Notifications} from 'expo';
export default function NotificationsTest() {
const y = 5;
const askPermissionsAsync = async () => {
await Permissions.askAsync(Permissions.USER_FACING_NOTIFICATIONS);
};
const btnSendNotClicked = async () => {
if(y===5){
await askPermissionsAsync();
Notifications.presentLocalNotificationAsync({
title: "Title",
body: "****** SUBJ *******",
ios:{
sound:true,
},
android:{
sound:true,
color:'#512da8',
vibrate:true,
}
});
}else{
} }
useEffect(()=>{
return btnSendNotClicked
}
,[])
return (
<View style={styles.container}>
</View>
);
}
and I just want to confirm is it good practice to checking this kind of condition in useEffect ?
Hi on startup useEffect with empty array will be fired and this will be happened during the page load (first time), so you can write your condition there. here is an example:
useEffect(()=> {
// Your condition here
}, []);
If you have a variable like value then you can write another useEffect like below and set that variable (value) in second parameter of useEffect as an array
const [value, setValue] = useState('');
useEffect(()=> {
// Your condition here
}, [value]);
const y = 5;
//this one will run every time the component renders
//so you could use it to check for anything on the startup
useEffect(()=> {
// Your condition here
}, []);
Otherwise you could add a value or array of values that you want to track for changes so this useEffect bellow Will only run if the y const changes so you could add an if check there to check if y===5 also this useEffect will run on the first initial of the const y so its perfect in your case
}
useEffect(()=> {
// will run every time y const change and on the first initial of y
if (y === 5)
{
//do your stuff here
}
}, [y]);

useState not setting after initial setting

I have a functional component that is using useState and uses the #react-google-maps/api component. I have a map that uses an onLoad function to initalize a custom control on the map (with a click event). I then set state within this click event. It works the first time, but every time after that doesn't toggle the value.
Function component:
import React, { useCallback } from 'react';
import { GoogleMap, LoadScript } from '#react-google-maps/api';
export default function MyMap(props) {
const [radiusDrawMode, setRadiusDrawMode] = React.useState(false);
const toggleRadiusDrawMode = useCallback((map) => {
map.setOptions({ draggableCursor: (!radiusDrawMode) ? 'crosshair' : 'grab' });
setRadiusDrawMode(!radiusDrawMode);
}, [setRadiusDrawMode, radiusDrawMode]); // Tried different dependencies.. nothing worked
const mapInit = useCallback((map) => {
var radiusDiv = document.createElement('div');
radiusDiv.index = 1;
var radiusButton = document.createElement('div');
radiusDiv.appendChild(radiusButton);
var radiusText = document.createElement('div');
radiusText.innerHTML = 'Radius';
radiusButton.appendChild(radiusText);
radiusButton.addEventListener('click', () => toggleRadiusDrawMode(map));
map.controls[window.google.maps.ControlPosition.RIGHT_TOP].push(radiusDiv);
}, [toggleRadiusDrawMode, radiusDrawMode, setRadiusDrawMode]); // Tried different dependencies.. nothing worked
return (
<LoadScript id="script-loader" googleMapsApiKey="GOOGLE_API_KEY">
<div className="google-map">
<GoogleMap id='google-map'
onLoad={(map) => mapInit(map)}>
</GoogleMap>
</div>
</LoadScript>
);
}
The first time the user presses the button on the map, it setss the radiusDrawMode to true and sets the correct cursor for the map (crosshair). Every click of the button after does not update radiusDrawMode and it stays in the true state.
I appreciate any help.
My guess is that it's a cache issue with useCallback. Try removing the useCallbacks to test without that optimization. If it works, you'll know for sure, and then you can double check what should be memoized and what maybe should not be.
I'd start by removing the one from toggleRadiusDrawMode:
const toggleRadiusDrawMode = map => {
map.setOptions({ draggableCursor: (!radiusDrawMode) ? 'crosshair' : 'grab' });
setRadiusDrawMode(!radiusDrawMode);
};
Also, can you access the state of the map options (the ones that you're setting with map.setOptions)? If so, it might be worth using the actual state of the map's option rather than creating your own internal state to track the same thing. Something like (I'm not positive that it would be map.options):
const toggleRadiusDrawMode = map => {
const { draggableCursor } = map.options;
map.setOptions({
draggableCursor: draggableCursor === 'grab' ? 'crosshair' : 'grab'
});
};
Also, I doubt this is the issue, but it looks like you're missing a closing bracket on the <GoogleMap> element? (Also, you might not need to create the intermediary function between onLoad and mapInit, and can probably pass mapInit directly to the onLoad.)
<GoogleMap id='google-map'
onLoad={mapInit}>
This is the solution I ended up using to solve this problem.
I basically had to switch out using a useState(false) for setRef(false). Then set up a useEffect to listen to changes on the ref, and in the actual toggleRadiusDraw I set the reference value which fires the useEffect to set the actual ref value.
import React, { useCallback, useRef } from 'react';
import { GoogleMap, LoadScript } from '#react-google-maps/api';
export default function MyMap(props) {
const radiusDrawMode = useRef(false);
let currentRadiusDrawMode = radiusDrawMode.current;
useEffect(() => {
radiusDrawMode.current = !radiusDrawMode;
});
const toggleRadiusDrawMode = (map) => {
map.setOptions({ draggableCursor: (!currentRadiusDrawMode) ? 'crosshair' : 'grab' });
currentRadiusDrawMode = !currentRadiusDrawMode;
};
const mapInit = (map) => {
var radiusDiv = document.createElement('div');
radiusDiv.index = 1;
var radiusButton = document.createElement('div');
radiusDiv.appendChild(radiusButton);
var radiusText = document.createElement('div');
radiusText.innerHTML = 'Radius';
radiusButton.appendChild(radiusText);
radiusButton.addEventListener('click', () => toggleRadiusDrawMode(map));
map.controls[window.google.maps.ControlPosition.RIGHT_TOP].push(radiusDiv);
});
return (
<LoadScript id="script-loader" googleMapsApiKey="GOOGLE_API_KEY">
<div className="google-map">
<GoogleMap id='google-map'
onLoad={(map) => mapInit(map)}>
</GoogleMap>
</div>
</LoadScript>
);
}
Not sure if this is the best way to handle this, but hope it helps someone else in the future.

Resources