How to remove React hook in certain conditions? - reactjs

Can't find a way to remove a hook when going to the next page section.
I've created a "useMousePosition" hook that tracks mouse position and returns mouse coordinates winch I use to transform some <div/>'s position. When scrolling down the page there's no need to transform the <div/> so I want to remove this useMousePosition hook.
useMouseHook
function useMousePosition() {
let [mousePosition, setMousePosition] = useState({
x: null,
y: null
});
function handleMouseMove(e) {
setMousePosition({
x: e.pageX,
y: e.pageY
});
}
useEffect(() => {
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, []);
return mousePosition;
}
Use hook in a component like so
let { x, y } = useMousePosition();
I need to remove a hook when user has scrolled to the next page section (Component with hook doesn't unmount)

The way I understood the question is that, you want to stop tracking the mouse movement.
If my understanding is correct, you can pass a flag to start/top tracking the mouse movement.
This demo shows that you can turn on/off the mouse tracking and
You can follow along
You can simply pass a variable, which you can check within your useEffect.
function useMousePosition(shouldTrack = true) {
let [mousePosition, setMousePosition] = useState({
x: null,
y: null
});
function handleMouseMove(e) {
setMousePosition({
x: e.pageX,
y: e.pageY
});
}
useEffect(() => {
if (!shouldTrack) return;
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, [shouldTrack]);
return mousePosition;
}
function App() {
const [useMouse, setUseMouse] = useState(true);
let { x, y } = useMousePosition(useMouse);
useEffect(() => {
console.log(`x, y`, x, y);
}, [x, y]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<button onClick={() => setUseMouse(_ => !_)}>
Tracking Mouse Movement is {useMouse ? "On" : "Off"}
</button>
</div>
);
}
Clicking on the button toggles the track status.
And for "removing the hook", you can't as it's embedded in your Function Component.
You can at least prevent the "side effect" from running using a condition.
⚠ Note that useEffect has a dependency as [shouldTrack].

You need to specify conditions in handleMouseMove
In the next solution, you stop render outside the pink border and remove the listener under the black line.
Note: added useCallback and dep array because of unnecessary renderings.
const isInsideBox = ({ pageX, pageY }) =>
LEFT <= pageX && pageX <= RIGHT && TOP <= pageY;
function useMousePosition() {
let [mousePosition, setMousePosition] = useState({
x: null,
y: null
});
const handleMouseMove = useCallback(
e => {
isInsideBox(e) && // Add Condition for Border
setMousePosition({
x: e.pageX,
y: e.pageY
});
e.pageY >= BOTTOM && // Add Condition for Black Line
window.removeEventListener("mousemove", handleMouseMove);
},
[setMousePosition]
);
useEffect(() => {
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, [handleMouseMove]);
return mousePosition;
}

Related

React useState not updating with setState function

I am trying to initialize useState hook with a class object then trying to update it based on a click eventListner but it is not updating more than one time.
function App() {
useEffect(() => {
const func = (e) => {
console.log('click')
handleKeyPress(10, 0)
}
document.addEventListener('click', func)
return () => document.removeEventListener('click', func)
}, [])
console.log('BEGIN')
const [player, setPlayer] = useState({
position: { x: 100, y: 100 },
direction: 'right',
})
console.log(player.position)
const handleKeyPress = (xChange, yChange) => {
const newX = player.position.x + xChange
const newY = player.position.y + yChange
let newDirection = player.direction
if (xChange > 0) {
newDirection = 'right'
}
if (xChange < 0) {
newDirection = 'left'
}
console.log('------->', player.position)
setPlayer((prev) => ({
...prev,
direction: newDirection,
position: { x: newX, y: newY },
}))
}
return (
<GameContainer className="game-container">
<Character positionx={player.position.x} positiony={player.position.y} />
</GameContainer>
)
}
export default App
console log can be seen below ->
as you can see the results it is not updating the x value of player state with every click also note that the string "BEGIN" is printing repeatedly with each click that means I think the render function is reinitiallizing the useState again and again.
useEffect(() => {
const func = (e) => {
console.log('click')
handleKeyPress(10, 0)
}
document.addEventListener('click', func)
return () => document.removeEventListener('click', func)
}, [])
The empty dependency array at the end of this use effect means you only want to run this effect once, when the component first mounts. So you will be using the version of handleKeyPress which existed at that time.
const newX = player.position.x + xChange
const newY = player.position.y + yChange
In handleKeyPress, you're using the player variable to calculate your new values. Since this is the handleKeyPress from the first render, it's also the player from the first render, so position.x and position.y are always 100.
There are two ways you can fix this. First, you can list your dependencies in your useEffect, so that it gets updated every time those dependencies change. In your case, that would be a dependency array of [handleKeyPress], which will actually change on every single render, so at that point you might as well leave out the dependency array entirely. You could also move handleKeyPress inside of the useEffect, and then change the dependency array to [player].
The second option is to make changes to handleKeyPress so it always uses the latest state. You're already using the function version of setPlayer to get the latest state, but you need to use it for all your calculations:
const handleKeyPress = (xChange, yChange) => {
setPlayer((prev) => {
const newX = prev.position.x + xChange;
const newY = prev.position.y + yChange;
let newDirection = prev.direction;
if (xChange > 0) {
newDirection = "right";
}
if (xChange < 0) {
newDirection = "left";
}
return {
...prev,
direction: newDirection,
position: { x: newX, y: newY },
};
});
};

Click handler on mapboxGL not respondig to updated state using react hooks with typescript

I am working on this simple hiking application, with display a map in a modal where i want to set a start point and end point in the modal components state, and then update the parents state.
The problem is that the click handler in the modal does not "see" the updated state.
If i console log the state outside of the click handler, it gives me the updated state.
Im using mapboxGL js, and i wonder if someone knows why this is happening? I am thinking maybe it as something to do with the 'click' event, since it not react onClick event?
Here is the code for the modal component:
export const MapModalContent = ({
newHike, setNewHike, setShowMapModal,
}: MapModalProps) => {
const [map, setMap] = useState<mapboxgl.Map>();
const [startCoords, setStartCoords] = useState<LngLat>();
const [endCoords, setEndCoords] = useState<LngLat>();
const mapRef: React.MutableRefObject<HTMLDivElement | null> = useRef(null);
const [helperString, setHelperString] = useState<IHelperString>(helperStrings[0]);
const [startMarker] = useState(new mapboxgl.Marker());
const [endMarker] = useState(new mapboxgl.Marker());
const [startPointIsSet, setStartPointIsSet] = useState<boolean>(false);
const [endPointIsSet, setEndPointIsSet] = useState<boolean>(false);
// initializes map
useEffect(() => {
if (mapRef.current && !map) {
setMap(new mapboxgl.Map({
accessToken: MAPBOX_ACCESS_TOKEN,
container: mapRef.current,
style: 'mapbox://styles/mapbox/outdoors-v11',
center: [10.748503539483494, 59.92003719905571],
zoom: 10,
}));
}
}, [mapRef]);
// adds controls and click listener to map
useEffect(() => {
if (map) {
addControls({ to: map });
map.on('click', (e) => {
handleSetMarker(e);
});
}
}, [map]);
// these effects console the updated state as wanted
useEffect(() => {
console.log('Start coord: ', startCoords);
}, [startCoords]);
useEffect(() => {
console.log('End coord: ', endCoords);
}, [endCoords]);
useEffect(() => {
console.log('Start is set: ', startPointIsSet);
}, [startPointIsSet]);
useEffect(() => {
console.log('End is set: ', endPointIsSet);
}, [endPointIsSet]);
// Todo: Remove this..
setTimeout(() => {
if (map) map.resize();
}, 500);
// this is the click handler that does not respond to state changes
const handleSetMarker = (e: mapboxgl.MapMouseEvent & mapboxgl.EventData) => {
console.log('👆', `start point: ${startPointIsSet}`, `end point: ${endPointIsSet}`);
if (!startPointIsSet) {
console.log('Start point not set.. Gonna set it now!');
// setStartCoords(e.lngLat);
startMarker.setLngLat(e.lngLat).addTo(map!);
setStartCoords(e.lngLat);
setStartPointIsSet(true);
}
if (startPointIsSet && !endPointIsSet) {
console.log('Start point is set! Setting end point..');
endMarker.setLngLat(e.lngLat).addTo(map!);
setEndCoords(e.lngLat);
setEndPointIsSet(true);
}
};
const handleButtonTap = () => {
if (startCoords) {
// displays a new message to the user after setting start point
setHelperString(helperStrings[1]);
} else {
console.warn('No start coords set!');
}
// sets parents state
if (startCoords && endCoords) {
setNewHike({
...newHike,
start_point: getPointString({ from: startCoords }),
end_point: getPointString({ from: endCoords }),
});
setShowMapModal(false);
} else {
console.warn('Some coords is null!', startCoords, endCoords);
}
};
return (
<>
<MapContainer ref={mapRef} />
<p style={{ margin: '1em auto 1em auto' }}>{ helperString.sentence }</p>
<IonButton onClick={handleButtonTap}>{ helperString.button }</IonButton>
</>
);
};
I've tried lifting the state up to the parent, but it gave me the exact same result.
I've also tried adding two separate click events to the map with no luck.
And I gave it a try with the 'react-mapbox-gl' wrapper, but the same problem arose.
It looks like because your handler is attached as a callback, it closes over the React state.
map.on('click', (e) => {
handleSetMarker(e);
});
Try useCallback with the proper dependencies.
const handleSetMarker = useCallback((e: mapboxgl.MapMouseEvent & mapboxgl.EventData) => {
console.log('👆', `start point: ${startPointIsSet}`, `end point: ${endPointIsSet}`);
if (!startPointIsSet) {
console.log('Start point not set.. Gonna set it now!');
// setStartCoords(e.lngLat);
startMarker.setLngLat(e.lngLat).addTo(map!);
setStartCoords(e.lngLat);
setStartPointIsSet(true);
}
if (startPointIsSet && !endPointIsSet) {
console.log('Start point is set! Setting end point..');
endMarker.setLngLat(e.lngLat).addTo(map!);
setEndCoords(e.lngLat);
setEndPointIsSet(true);
}
}, [startPointIsSet, endPointIsSet, endMarker, startMarker, map]);

Modal trigger - mouse position

I have a question about React. How can I trigger a certain function in the useEffect hook, depending on the position of the cursor?
I need to trigger a function that will open a popup in two cases - one thing is to open a modal after a certain time (which I succeeded with a timeout method), but the other case is to trigger the modal opening function once the mouse is at the very top of the website right in the middle. Thanks for the help.
For now I have that, but I'm struggling with the mouse position part:
function App(e) {
const [modalOpened, setModalOpened] = useState(false);
console.log(e)
useEffect(() => {
const timeout = setTimeout(() => {
setModalOpened(true);
}, 30000);
return () => clearTimeout(timeout);
}, []);
const handleCloseModal = () => setModalOpened(false);
You could use a custom hook that checks the conditions you want.
I'm sure this could be cleaner, but this hook works.
export const useMouseTopCenter = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isTopCenter, setIsTopCenter] = useState(false);
const width = window.innerWidth;
// Tracks mouse position
useEffect(() => {
const setFromEvent = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener("mousemove", setFromEvent);
return () => {
window.removeEventListener("mousemove", setFromEvent);
};
}, []);
// Tracks whether mouse position is in the top 100px and middle third of the screen.
useEffect(() => {
const centerLeft = width / 3;
const centerRight = (width / 3) * 2;
if (
position.x > centerLeft &&
position.x < centerRight &&
position.y < 100
) {
setIsTopCenter(true);
} else {
setIsTopCenter(false);
}
}, [position, width]);
return isTopCenter;
};
Then you would simply add const isCenterTop = useMouseTopCenter(); to your app component.
Checkout this code sandbox, which is just a variation of this custom hook.

React Hooks onChange not accepting input

I have a weird bug that only happens some of the time - onChange fires but does not change the value. Then if I click outside of the input with the onChange function, then click back inside the input box, the onChange function starts working.
The onChange function is like so:
const handleBarAmountChange = (event) => {
let newWidthAmount = event.target.value / 10;
setNewWidth(newWidthAmount);
setNewBarAmount(event.target.value);
};
A parent div is using a ref with useRef that is passed to this function:
import { useEffect, useState } from 'react';
const useMousePosition = (barRef, barInputRef, barContainerRef) => {
const [ mouseIsDown, setMouseIsDown ] = useState(null);
useEffect(() => {
const setMouseDownEvent = (e) => {
if (e.which == 1) {
if (barContainerRef.current.contains(e.target) && !barInputRef.current.contains(e.target)) {
setMouseIsDown(e.clientX);
} else if (!barInputRef.current.contains(e.target)) {
setMouseIsDown(null);
}
}
};
window.addEventListener('mousemove', setMouseDownEvent);
return () => {
window.removeEventListener('mousemove', setMouseDownEvent);
};
}, []);
return { mouseIsDown };
};
Is the onChange conflicting somehow with the eventListener?
How do I get round this?
There were a few syntax errors and missing hook dependencies that were the cause of your bugs. However, you can simplify your code quite a bit with a few tweaks.
When using state that relies upon other state, I recommend lumping it into an object and using a callback function to synchronously update it: setState(prevState => ({ ...prevState, example: "newValue" }). This is similar to how this.setState(); works in a class based component. By using a single object and spreading out it properties ({ ...prevState }), we can then overwrite one of its properties by redefining one of them ({ ...prevState, newWidth: 0 }). This way ensures that the values are in sync with each other.
The example below follows the single object pattern mentioned above, where newWidth, newBarAmount and an isDragging are properties of a single object (state). Then, the example uses setState to update/override the values synchronously. In addition, the refs have been removed and allow the bar to be dragged past the window (if you don't want this, then you'll want to confine it within the barContainerRef as you've done previously). The example also checks for a state.isDragging boolean when the user left mouse clicks and holds on the bar. Once the left click is released, the dragging is disabled.
Here's a working example:
components/Bar/index.js
import React, { useEffect, useState, useCallback } from "react";
import PropTypes from "prop-types";
import "./Bar.css";
function Bar({ barName, barAmount, colour, maxWidth }) {
const [state, setState] = useState({
newWidth: barAmount / 2,
newBarAmount: barAmount,
isDragging: false
});
// manual input changes
const handleBarAmountChange = useCallback(
({ target: { value } }) => {
setState(prevState => ({
...prevState,
newWidth: value / 2,
newBarAmount: value
}));
},
[]
);
// mouse move
const handleMouseMove = useCallback(
({ clientX }) => {
if (state.isDragging) {
setState(prevState => ({
...prevState,
newWidth: clientX > 0 ? clientX / 2 : 0,
newBarAmount: clientX > 0 ? clientX : 0
}));
}
},
[state.isDragging]
);
// mouse left click hold
const handleMouseDown = useCallback(
() => setState(prevState => ({ ...prevState, isDragging: true })),
[]
);
// mouse left click release
const handleMouseUp = useCallback(() => {
if (state.isDragging) {
setState(prevState => ({
...prevState,
isDragging: false
}));
}
}, [state.isDragging]);
useEffect(() => {
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};
}, [handleMouseMove, handleMouseUp]);
return (
<div className="barContainer">
<div className="barName">{barName}</div>
<div
style={{ cursor: state.isDragging ? "grabbing" : "pointer" }}
onMouseDown={handleMouseDown}
className="bar"
>
<svg
width={state.newWidth > maxWidth ? maxWidth : state.newWidth}
height="40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
colour={colour}
>
<rect width={state.newWidth} height="40" fill={colour} />
</svg>
</div>
<div className="barAmountUnit">£</div>
<input
className="barAmount"
type="number"
value={state.newBarAmount}
onChange={handleBarAmountChange}
/>
</div>
);
}
// default props (will be overridden if defined)
Bar.defaultProps = {
barAmount: 300,
maxWidth: 600
};
// check that passed in props match patterns below
Bar.propTypes = {
barName: PropTypes.string,
barAmount: PropTypes.number,
colour: PropTypes.string,
maxWidth: PropTypes.number
};
export default Bar;
React uses SyntheticEvent and Event Pooling, from the doc:
Event Pooling
The SyntheticEvent is pooled. This means that the SyntheticEvent object will be reused and all properties will be nullified after the event callback has been invoked. This is for performance reasons. As such, you cannot access the event in an asynchronous way.
You could call event.persist() on the event or store the value in a new variable and use it as follows:
const handleBarAmountChange = (event) => {
// event.persist();
// Or
const { value } = event.target;
let newWidthAmount = value / 10;
setNewWidth(newWidthAmount);
setNewBarAmount(value);
};

Accessing values from an eventListener inside a React Hook with an empty dependancy array

I am attempting to use React hooks to run a canvas animation that depends on mouse position. I am using a custom hook for mouse position, and wrote another custom hook to animate the canvas.
Placing an empty dependancy array in the animation hook keeps it from unmounting and remounting the animation loop anytime the mouse moves, as is mentioned in this tutorial and suggested in a note in the React docs.
So the code below works, in that I can access coords inside the drawNow() function, but mounting and unmounting the animation loop every time the mouse moves does not seem like an acceptable way to do things.
How does one access event listeners inside React hooks that are purposely set to have no dependancies?
Here is the animation and the draw function....
const drawNow = (context,coords) => {
context.fillStyle = '#fff';
context.beginPath();
context.arc(coords.x,coords.y,50,0,2*Math.PI); // need coords here
context.fill();
}
export const Canvas = () => {
let ref = React.useRef();
// custom hook that returns mouse position
const coords = useMouseMove();
React.useEffect(() => {
let canvas = ref.current;
let context = canvas.getContext('2d');
const render = () => {
aId = requestAnimationFrame(render);
drawNow(context,coords); // requires current mouse coordinates
};
let aId = requestAnimationFrame(render);
return () => cancelAnimationFrame(aId);
}, [coords]); // dependancy array should be left blank so requestAnimationFrame mounts only once?
return (
<canvas ref={ref}/>
style={{
width: '100%',
height: '100%',
}}
);
};
Here is the custom hook for the mouse coordinates (references this useEventListener)
export const useMouseMove = () => {
function getCoords(clientX,clientY) {
return {
x: clientX || 0,
y: clientY || 0
};
}
const [coords, setCoords] = useState(getCoords);
useEventListener('mousemove', ({ clientX, clientY }) => {
setCoords(getCoords(clientX,clientY));
});
return coords;
};
Thanks, and looking forward to understanding more about hooks and event listeners.
Okay, I figured out my problem. The issue is the useMouseMove() hook was updating the coordinates with useState, when really I wanted to be using useRef, allowing me to imperatively "modify a child outside the typical data flow", as mentioned here.
First, I combined the useMouseMove() hook with the more general useEventListener so I didn't have to navigate unnecessary abstractions:
// a function that keeps track of mouse coordinates with useState()
export const useMouseMove = () => {
function getCoords(clientX,clientY) {
return {
x: clientX || 0,
y: clientY || 0
};
}
const [coords, setCoords] = useState(getCoords);
useEffect(
() => {
function handleMove(e) {
setCoords(getCoords(e.clientX,e.clientY));
}
global.addEventListener('mousemove', handleMove);
return () => {
global.removeEventListener('mousemove', handleMove);
};
}
);
return coords;
};
The next step was to "convert" the above function from useState() to useRef():
// a function that keeps track of mouse coordinates with useRef()
export const useMouseMove = () => {
function getCoords(clientX,clientY) {
return {
x: clientX || 0,
y: clientY || 0
};
}
const coords = useRef(getCoords); // ref not state!
useEffect(
() => {
function handleMove(e) {
coords.current = getCoords(e.clientX,e.clientY);
}
global.addEventListener('mousemove', handleMove);
return () => {
global.removeEventListener('mousemove', handleMove);
};
}
);
return coords;
};
Finally, I can access the mouse coordinates inside the animation loop while also keeping the dependancy array blank, preventing the animation component from remounting every time the mouse moves.
// the animation loop that mounts only once with mouse coordinates
export const Canvas = () => {
let ref = React.useRef();
// custom hook that returns mouse position
const coords = useMouseMove();
React.useEffect(() => {
let canvas = ref.current;
let context = canvas.getContext('2d');
const render = () => {
aId = requestAnimationFrame(render);
drawNow(context,coords.current); // mouse coordinates from useRef()
};
let aId = requestAnimationFrame(render);
return () => cancelAnimationFrame(aId);
}, []); // dependancy array is blank, the animation loop mounts only once
return (
<canvas ref={ref}/>
style={{
width: '100%',
height: '100%',
}}
);
};
Thanks to this escape hatch, I am able to create endless web fun without remounting the animation loop.

Resources