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.
Related
I'm trying to achieve smooth animation of searchbar (MUI Autocomplete). This should work only on Smartphones (Screen < 600px).
Here is an example (it is very buggy and open it on smartphone to see the animation): https://react-zxuspr-gjq5w8.stackblitz.io/
And here is my implementation, but I've a few problems with that:
The interval does not reset on dropdown close.
The React.useEffect() dependency is set to searchActive, which is changed dynamically.
I tried calling the callback function of React.useState(), but since the component is not destroyed, I am not sure if it makes sense.
The width of dropdown, which is also changed in the setInterval() function, is not smooth at all.
https://stackblitz.com/edit/react-zxuspr-gjq5w8?file=demo.js
Here is part of the component where the logic is implemented:
export function PrimarySearchAppBar() {
const [searchActive, setSearchActive] = React.useState(null);
const [acPaperWidth, setAcPaperWidth] = React.useState(null);
const [acPaperTransX, setAcPaperTransX] = React.useState(0);
const AcRef = React.useRef(null);
const isMobile = useMediaQuery(useTheme().breakpoints.down('sm'));
const options = top100Films.map((option) => {
const group = option.group.toUpperCase();
return {
firstLetter: group,
...option,
};
});
React.useLayoutEffect(() => {
if (AcRef.current) {
setAcPaperWidth(AcRef.current.offsetWidth);
}
console.log(acPaperWidth);
}, [AcRef]);
let interval;
React.useEffect(() => {
if (searchActive) {
if (acPaperTransX <= 39) {
interval = setInterval(() => {
setAcPaperWidth(AcRef.current.offsetWidth);
setAcPaperWidth((acPaperTransX) => acPaperTransX + 1);
if (acPaperTransX >= 38) {
clearInterval(interval);
}
console.log(acPaperTransX);
}, 10);
}
} else {
setAcPaperTransX(0);
clearInterval(interval);
}
}, [searchActive]);
return (
<>Hello World</>
);
}
The problem was that the interval variable was defined outside the React.useEffect() hook, so its value was not preserved on re-renders.
I was able to fix that by using React.useRef():
const intervalRef = React.useRef();
const acPaperTransXRef = React.useRef(acPaperTransX);
React.useEffect(() => {
if (searchActive) {
if (acPaperTransXRef.current <= 39) {
intervalRef.current = setInterval(() => {
setAcPaperWidth(AcRef.current.offsetWidth);
acPaperTransXRef.current += 0.1;
if (acPaperTransXRef.current >= 38) {
clearInterval(intervalRef.current);
acPaperTransXRef.current = 0;
}
console.log(acPaperTransXRef.current);
}, 2);
}
} else {
setAcPaperTransX(0);
acPaperTransXRef.current = 0;
clearInterval(intervalRef.current);
}
return () => clearInterval(intervalRef.current);
}, [searchActive]);
I'm trying to create an animation that hides the menu when the user scrolls down and when the user returns to the top the menu appears.
Everything works, but the problem comes when I try to give to the scrollY property that gives the useScroll hook a type.
This is the code that I could not find the way to implement the type.
const { scrollY } = useScroll();
const [isScrolling, setScrolling] = useState<boolean>(false);
const handleScroll = () => {
if (scrollY?.current < scrollY?.prev) setScrolling(false);
if (scrollY?.current > 100 && scrollY?.current > scrollY?.prev) setScrolling(true);
};
useEffect(() => {
return scrollY.onChange(() => handleScroll());
});
I found the solution!
Reading the documentation framer, I saw that they updated the way to get the value of the previous and current scroll.
scrollY.get() -> Current
scrollY.getPrevious() -> Previous
const { scrollY } = useScroll();
const [isScrolling, setScrolling] = useState<boolean>(false);
const handleScroll = () => {
if (scrollY.get() < scrollY.getPrevious()) setScrolling(false);
if (scrollY.get() > 100 && scrollY.get() > scrollY.getPrevious()) setScrolling(true);
};
useEffect(() => {
return scrollY.onChange(() => handleScroll());
});
When I click, I set the saveMouseDown state to 1, when I release I set it to 0.
When I click and move the mouse I log out mouseDown and it's 0 even when my mouse is down? Yet on the screen it shows 1
import React, { useEffect, useRef, useState } from 'react';
const Home: React.FC = () => {
const [mouseDown, saveMouseDown] = useState(0);
const [canvasWidth, saveCanvasWidth] = useState(window.innerWidth);
const [canvasHeight, saveCanvasHeight] = useState(window.innerHeight);
const canvasRef = useRef<HTMLCanvasElement>(null);
let canvas: HTMLCanvasElement;
let ctx: CanvasRenderingContext2D | null;
const addEventListeners = () => {
canvas.addEventListener('mousedown', (e) => { toggleMouseDown(); }, true);
canvas.addEventListener('mouseup', (e) => { toggleMouseUp(); }, true);
};
const toggleMouseDown = () => saveMouseDown(1);
const toggleMouseUp = () => saveMouseDown(0);
const printMouse = () => console.log(mouseDown);
// ^------ Why does this print the number 1 and the 2x 0 and then 1... and not just 1?
const removeEventListeners = () => {
canvas.removeEventListener('mousedown', toggleMouseDown);
canvas.removeEventListener('mouseup', toggleMouseUp);
};
useEffect(() => {
if (canvasRef.current) {
canvas = canvasRef.current;
ctx = canvas.getContext('2d');
addEventListeners();
}
return () => removeEventListeners();
}, []);
useEffect(() => {
if (canvasRef.current) {
canvas = canvasRef.current;
canvas.addEventListener('mousemove', (e) => { printMouse(); }, true );
}
return () => canvas.removeEventListener('mousemove', printMouse);
}, [mouseDown, printMouse]);
return (
<React.Fragment>
<p>Mouse Down: {mouseDown}</p>
{/* ^------ When this does print 1? */}
<canvas
id='canvas'
ref={canvasRef}
width={canvasWidth}
height={canvasHeight}
/>
</React.Fragment>
);
};
export { Home };
You only add the move listener once when the component mounted, thus enclosing the initial mouseDown value.
Try using a second useEffect hook to specifically set/update the onMouseMove event listener when the mouseDown state changes. The remove eventListener needs to specify the same callback.
useEffect(() => {
if (canvasRef.current) {
canvas = canvasRef.current;
canvas.addEventListener('mousemove', printMouse, true );
}
return () => canvas.removeEventListener('mousemove', printMouse);;
}, [mouseDown, printMouse]);
It may be simpler to attach the event listeners directly on the canvas element, then you don't need to worry about working with enclosed stale state as much with the effect hooks.
<canvas
onMouseDown={() => setMouseDown(1)}
onMouseUp={() => setMouseDown(0)}
onMouseMove={printMouse}
width={canvasWidth}
height={canvasHeight}
/>
So here is the code ...
function getWindowDimensions() {
const { innerWidth: width, innerHeight: height } = window;
return {
width,
height
};
}
let minNumberOfCards = Math.round(window.innerWidth / 120);
const [windowDimensions, setWindowDimensions] = React.useState(getWindowDimensions());
React.useEffect(() => {
function handleResize() {
setWindowDimensions(getWindowDimensions());
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [windowDimensions,]);
The issue I'm facing is that I need to compute the number of cards to be displayed and I get the a really messy result :|. Since addEventListener is an async thingy.
What I tried is minNumberOfCards = Math.round(windowDimensions.width/ 120);
buuuut again since it's async I get the correct number after a while time when my initial numbers I set kicks in. Basically I've set a var minNumberOfCards to be default 9 ( for example ).
The expected result would be to have the minNumberOfCards depending on the screen width. That's it !!!!! Any ideas/solutions on how can I achieve this without causing lots of re-renderings ?
Its a perfect use case for debounce or throttle:
import debounce from 'lodash.debounce';
const App = () => {
const [windowDimensions, setWindowDimensions] = useState('Change Window Size');
const [minNumberOfCards, setMinNumberOfCards] = useState(
Math.round(window.innerWidth / 120)
);
useEffect(() => {
// Better add a listener and only change the minNumberOfCards
},[...]);
useEffect(() => {
const handleResize = () => setWindowDimensions(getWindowDimensions());
const debouncedHandleResize = debounce(handleResize, 500);
window.addEventListener('resize', debouncedHandleResize);
return () => window.removeEventListener('resize', handleResize);
}, [windowDimensions]);
return (
<FlexBox>
<pre>{JSON.stringify(windowDimensions, null, 2)}</pre>
</FlexBox>
);
};
Try changing the window size in the next sandbox:
Please refer to Debouncing and Throttling Explained Through Examples
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.